aboutsummaryrefslogtreecommitdiff
path: root/azalea-protocol/src
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2021-12-15 23:10:55 -0600
committermat <github@matdoes.dev>2021-12-15 23:10:55 -0600
commit9642558f8f8d983a7087f15d68be8cf07a85f0c2 (patch)
tree5f0a967f005cd5db510a13ab290c8ad6669b25aa /azalea-protocol/src
parent72aefe871ca4983431b1a0b707b472e73ffea836 (diff)
downloadazalea-drasl-9642558f8f8d983a7087f15d68be8cf07a85f0c2.tar.xz
azalea
Diffstat (limited to 'azalea-protocol/src')
-rw-r--r--azalea-protocol/src/connect.rs116
-rw-r--r--azalea-protocol/src/lib.rs56
-rw-r--r--azalea-protocol/src/mc_buf.rs203
-rw-r--r--azalea-protocol/src/packets/game/mod.rs35
-rw-r--r--azalea-protocol/src/packets/handshake/client_intention_packet.rs36
-rw-r--r--azalea-protocol/src/packets/handshake/mod.rs49
-rw-r--r--azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs41
-rw-r--r--azalea-protocol/src/packets/login/clientbound_hello_packet.rs38
-rw-r--r--azalea-protocol/src/packets/login/mod.rs63
-rw-r--r--azalea-protocol/src/packets/login/serverbound_hello_packet.rs27
-rw-r--r--azalea-protocol/src/packets/mod.rs146
-rw-r--r--azalea-protocol/src/packets/status/clientbound_status_response_packet.rs58
-rw-r--r--azalea-protocol/src/packets/status/mod.rs66
-rw-r--r--azalea-protocol/src/packets/status/serverbound_status_request_packet.rs23
-rw-r--r--azalea-protocol/src/read.rs28
-rw-r--r--azalea-protocol/src/resolver.rs55
-rw-r--r--azalea-protocol/src/write.rs27
17 files changed, 1067 insertions, 0 deletions
diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs
new file mode 100644
index 00000000..f6dd9fe7
--- /dev/null
+++ b/azalea-protocol/src/connect.rs
@@ -0,0 +1,116 @@
+//! parse sending and receiving packets with a server.
+
+use crate::packets::game::GamePacket;
+use crate::packets::handshake::HandshakePacket;
+use crate::packets::login::LoginPacket;
+use crate::packets::status::StatusPacket;
+use crate::read::read_packet;
+use crate::write::write_packet;
+use crate::ServerIpAddress;
+use tokio::net::TcpStream;
+
+pub enum PacketFlow {
+ ClientToServer,
+ ServerToClient,
+}
+
+pub struct HandshakeConnection {
+ pub flow: PacketFlow,
+ /// The buffered writer
+ pub stream: TcpStream,
+}
+
+pub struct GameConnection {
+ pub flow: PacketFlow,
+ /// The buffered writer
+ pub stream: TcpStream,
+}
+
+pub struct StatusConnection {
+ pub flow: PacketFlow,
+ /// The buffered writer
+ pub stream: TcpStream,
+}
+
+pub struct LoginConnection {
+ pub flow: PacketFlow,
+ /// The buffered writer
+ pub stream: TcpStream,
+}
+
+impl HandshakeConnection {
+ pub async fn new(address: &ServerIpAddress) -> Result<HandshakeConnection, 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(HandshakeConnection {
+ flow: PacketFlow::ServerToClient,
+ stream,
+ })
+ }
+
+ pub fn login(self) -> LoginConnection {
+ LoginConnection {
+ flow: self.flow,
+ stream: self.stream,
+ }
+ }
+
+ pub fn status(self) -> StatusConnection {
+ StatusConnection {
+ flow: self.flow,
+ stream: self.stream,
+ }
+ }
+
+ pub async fn read(&mut self) -> Result<HandshakePacket, String> {
+ read_packet::<HandshakePacket>(&self.flow, &mut self.stream).await
+ }
+
+ /// Write a packet to the server
+ pub async fn write(&mut self, packet: HandshakePacket) {
+ write_packet(packet, &mut self.stream).await;
+ }
+}
+
+impl GameConnection {
+ pub async fn read(&mut self) -> Result<GamePacket, String> {
+ read_packet::<GamePacket>(&self.flow, &mut self.stream).await
+ }
+
+ /// Write a packet to the server
+ pub async fn write(&mut self, packet: GamePacket) {
+ write_packet(packet, &mut self.stream).await;
+ }
+}
+
+impl StatusConnection {
+ pub async fn read(&mut self) -> Result<StatusPacket, String> {
+ read_packet::<StatusPacket>(&self.flow, &mut self.stream).await
+ }
+
+ /// Write a packet to the server
+ pub async fn write(&mut self, packet: StatusPacket) {
+ write_packet(packet, &mut self.stream).await;
+ }
+}
+
+impl LoginConnection {
+ pub async fn read(&mut self) -> Result<LoginPacket, String> {
+ read_packet::<LoginPacket>(&self.flow, &mut self.stream).await
+ }
+
+ /// Write a packet to the server
+ pub async fn write(&mut self, packet: LoginPacket) {
+ write_packet(packet, &mut self.stream).await;
+ }
+}
diff --git a/azalea-protocol/src/lib.rs b/azalea-protocol/src/lib.rs
new file mode 100644
index 00000000..684add45
--- /dev/null
+++ b/azalea-protocol/src/lib.rs
@@ -0,0 +1,56 @@
+//! This lib is responsible for parsing Minecraft packets.
+
+use std::net::IpAddr;
+use std::str::FromStr;
+
+pub mod connect;
+pub mod mc_buf;
+pub mod packets;
+pub mod read;
+pub mod resolver;
+pub mod write;
+
+#[derive(Debug)]
+pub struct ServerAddress {
+ pub host: String,
+ pub port: u16,
+}
+
+#[derive(Debug)]
+pub struct ServerIpAddress {
+ pub ip: IpAddr,
+ pub port: u16,
+}
+
+// impl try_from for ServerAddress
+impl<'a> TryFrom<&'a str> for ServerAddress {
+ type Error = String;
+
+ /// Convert a Minecraft server address (host:port, the port is optional) to a ServerAddress
+ fn try_from(string: &str) -> Result<Self, Self::Error> {
+ 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/azalea-protocol/src/mc_buf.rs b/azalea-protocol/src/mc_buf.rs
new file mode 100644
index 00000000..54ba1f7d
--- /dev/null
+++ b/azalea-protocol/src/mc_buf.rs
@@ -0,0 +1,203 @@
+//! Utilities for reading and writing for the Minecraft protocol
+
+use std::io::Write;
+
+use async_trait::async_trait;
+use byteorder::{BigEndian, WriteBytesExt};
+use tokio::io::{AsyncRead, AsyncReadExt};
+
+// const DEFAULT_NBT_QUOTA: u32 = 2097152;
+const MAX_STRING_LENGTH: u16 = 32767;
+// const MAX_COMPONENT_STRING_LENGTH: u32 = 262144;
+
+#[async_trait]
+pub trait Writable {
+ fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error>;
+ fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>;
+ fn write_varint(&mut self, value: i32) -> Result<(), std::io::Error>;
+ fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error>;
+ fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error>;
+ fn write_short(&mut self, n: u16) -> Result<(), std::io::Error>;
+ fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error>;
+}
+
+#[async_trait]
+impl Writable for Vec<u8> {
+ fn write_byte(&mut self, n: u8) -> Result<(), std::io::Error> {
+ WriteBytesExt::write_u8(self, n)
+ }
+
+ fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> {
+ Ok(self.extend_from_slice(bytes))
+ }
+
+ fn write_varint(&mut self, mut value: i32) -> Result<(), std::io::Error> {
+ let mut buffer = [0];
+ if value == 0 {
+ self.write_all(&buffer).unwrap();
+ }
+ while value != 0 {
+ buffer[0] = (value & 0b0111_1111) as u8;
+ value = (value >> 7) & (i32::max_value() >> 6);
+ if value != 0 {
+ buffer[0] |= 0b1000_0000;
+ }
+ self.write_all(&buffer)?;
+ }
+ Ok(())
+ }
+
+ fn write_utf_with_len(&mut self, string: &str, len: usize) -> Result<(), std::io::Error> {
+ if string.len() > len {
+ panic!(
+ "String too big (was {} bytes encoded, max {})",
+ string.len(),
+ len
+ );
+ }
+ self.write_varint(string.len() as i32);
+ self.write_bytes(string.as_bytes())
+ }
+
+ fn write_utf(&mut self, string: &str) -> Result<(), std::io::Error> {
+ self.write_utf_with_len(string, MAX_STRING_LENGTH.into())
+ }
+
+ fn write_short(&mut self, n: u16) -> Result<(), std::io::Error> {
+ WriteBytesExt::write_u16::<BigEndian>(self, n)
+ }
+
+ fn write_byte_array(&mut self, bytes: &[u8]) -> Result<(), std::io::Error> {
+ self.write_varint(bytes.len() as i32);
+ self.write_bytes(bytes)
+ }
+}
+
+#[async_trait]
+pub trait Readable {
+ async fn read_varint(&mut self) -> Result<(i32, u8), String>;
+ async fn read_byte_array(&mut self) -> Result<Vec<u8>, String>;
+ async fn read_bytes(&mut self, n: usize) -> Result<Vec<u8>, String>;
+ async fn read_utf(&mut self) -> Result<String, String>;
+ async fn read_utf_with_len(&mut self, max_length: u32) -> Result<String, String>;
+ async fn read_byte(&mut self) -> Result<u8, String>;
+}
+
+#[async_trait]
+impl<R> Readable for R
+where
+ R: AsyncRead + std::marker::Unpin + std::marker::Send,
+{
+ // fast varints stolen from https://github.com/luojia65/mc-varint/blob/master/src/lib.rs#L67
+ /// Read a single varint from the reader and return the value, along with the number of bytes read
+ async fn read_varint(&mut self) -> Result<(i32, u8), String> {
+ let mut buffer = [0];
+ let mut ans = 0;
+ for i in 0..4 {
+ self.read_exact(&mut buffer)
+ .await
+ .map_err(|_| "Invalid VarInt".to_string())?;
+ ans |= ((buffer[0] & 0b0111_1111) as i32) << (7 * i);
+ if buffer[0] & 0b1000_0000 == 0 {
+ return Ok((ans, i + 1));
+ }
+ }
+ Ok((ans, 5))
+ }
+
+ async fn read_byte_array(&mut self) -> Result<Vec<u8>, String> {
+ let length = self.read_varint().await?.0 as usize;
+ Ok(self.read_bytes(length).await?)
+ }
+
+ async fn read_bytes(&mut self, n: usize) -> Result<Vec<u8>, String> {
+ let mut bytes = vec![0; n];
+ match AsyncReadExt::read_exact(self, &mut bytes).await {
+ Ok(_) => Ok(bytes),
+ Err(_) => Err("Error reading bytes".to_string()),
+ }
+ }
+
+ async fn read_utf(&mut self) -> Result<String, String> {
+ self.read_utf_with_len(MAX_STRING_LENGTH.into()).await
+ }
+
+ async fn read_utf_with_len(&mut self, max_length: u32) -> Result<String, String> {
+ let (length, _length_varint_length) = self.read_varint().await?;
+ // i don't know why it's multiplied by 4 but it's like that in mojang's code so
+ if length < 0 {
+ return Err(
+ "The received encoded string buffer length is less than zero! Weird string!"
+ .to_string(),
+ );
+ }
+ if length as u32 > max_length * 4 {
+ return Err(format!(
+ "The received encoded string buffer length is longer than maximum allowed ({} > {})",
+ length,
+ max_length * 4
+ ));
+ }
+
+ // this is probably quite inefficient, idk how to do it better
+ let mut string = String::new();
+ let mut buffer = vec![0; length as usize];
+ self.read_exact(&mut buffer)
+ .await
+ .map_err(|_| "Invalid UTF-8".to_string())?;
+
+ string.push_str(std::str::from_utf8(&buffer).unwrap());
+ if string.len() > length as usize {
+ return Err(format!(
+ "The received string length is longer than maximum allowed ({} > {})",
+ length, max_length
+ ));
+ }
+
+ Ok(string)
+ }
+
+ /// Read a single byte from the reader
+ async fn read_byte(&mut self) -> Result<u8, String> {
+ match AsyncReadExt::read_u8(self).await {
+ Ok(r) => Ok(r),
+ Err(_) => Err("Error reading byte".to_string()),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Cursor;
+ use tokio::io::BufReader;
+
+ #[test]
+ fn test_write_varint() {
+ let mut buf = Vec::new();
+ buf.write_varint(123456);
+ assert_eq!(buf, vec![192, 196, 7]);
+
+ let mut buf = Vec::new();
+ buf.write_varint(0);
+ assert_eq!(buf, vec![0]);
+ }
+
+ #[tokio::test]
+ async fn test_read_varint() {
+ let mut buf = BufReader::new(Cursor::new(vec![192, 196, 7]));
+ assert_eq!(buf.read_varint().await.unwrap(), (123456, 3));
+
+ let mut buf = BufReader::new(Cursor::new(vec![0]));
+ assert_eq!(buf.read_varint().await.unwrap(), (0, 1));
+
+ let mut buf = BufReader::new(Cursor::new(vec![1]));
+ assert_eq!(buf.read_varint().await.unwrap(), (1, 1));
+ }
+
+ #[tokio::test]
+ async fn test_read_varint_longer() {
+ let mut buf = BufReader::new(Cursor::new(vec![138, 56, 0, 135, 56, 123]));
+ assert_eq!(buf.read_varint().await.unwrap(), (7178, 2));
+ }
+}
diff --git a/azalea-protocol/src/packets/game/mod.rs b/azalea-protocol/src/packets/game/mod.rs
new file mode 100644
index 00000000..a3ef2541
--- /dev/null
+++ b/azalea-protocol/src/packets/game/mod.rs
@@ -0,0 +1,35 @@
+use async_trait::async_trait;
+use tokio::io::BufReader;
+
+use crate::connect::PacketFlow;
+
+use super::ProtocolPacket;
+
+#[derive(Clone, Debug)]
+pub enum GamePacket
+where
+ Self: Sized, {}
+
+#[async_trait]
+impl ProtocolPacket for GamePacket {
+ fn id(&self) -> u32 {
+ 0x00
+ }
+
+ fn write(&self, _buf: &mut Vec<u8>) {}
+
+ /// Read a packet by its id, ConnectionProtocol, and flow
+ async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ _id: u32,
+ flow: &PacketFlow,
+ _buf: &mut BufReader<T>,
+ ) -> Result<GamePacket, String>
+ where
+ Self: Sized,
+ {
+ match flow {
+ PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()),
+ PacketFlow::ClientToServer => Err("HandshakePacket::read not implemented".to_string()),
+ }
+ }
+}
diff --git a/azalea-protocol/src/packets/handshake/client_intention_packet.rs b/azalea-protocol/src/packets/handshake/client_intention_packet.rs
new file mode 100644
index 00000000..868626b3
--- /dev/null
+++ b/azalea-protocol/src/packets/handshake/client_intention_packet.rs
@@ -0,0 +1,36 @@
+use std::hash::Hash;
+
+use tokio::io::BufReader;
+
+use crate::{mc_buf::Writable, packets::ConnectionProtocol};
+
+use super::HandshakePacket;
+
+#[derive(Hash, Clone, Debug)]
+pub struct ClientIntentionPacket {
+ pub protocol_version: u32,
+ pub hostname: String,
+ pub port: u16,
+ /// 1 for status, 2 for login
+ pub intention: ConnectionProtocol,
+}
+
+impl ClientIntentionPacket {
+ pub fn get(self) -> HandshakePacket {
+ HandshakePacket::ClientIntentionPacket(self)
+ }
+
+ pub fn write(&self, buf: &mut Vec<u8>) {
+ buf.write_varint(self.protocol_version as i32).unwrap();
+ buf.write_utf(&self.hostname).unwrap();
+ buf.write_short(self.port).unwrap();
+ buf.write_varint(self.intention.clone() as i32).unwrap();
+ }
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ _buf: &mut BufReader<T>,
+ ) -> Result<HandshakePacket, String> {
+ Err("ClientIntentionPacket::parse not implemented".to_string())
+ // Ok(ClientIntentionPacket {}.get())
+ }
+}
diff --git a/azalea-protocol/src/packets/handshake/mod.rs b/azalea-protocol/src/packets/handshake/mod.rs
new file mode 100644
index 00000000..01010e1e
--- /dev/null
+++ b/azalea-protocol/src/packets/handshake/mod.rs
@@ -0,0 +1,49 @@
+pub mod client_intention_packet;
+
+use async_trait::async_trait;
+use tokio::io::BufReader;
+
+use crate::connect::PacketFlow;
+
+use super::ProtocolPacket;
+
+#[derive(Clone, Debug)]
+pub enum HandshakePacket
+where
+ Self: Sized,
+{
+ ClientIntentionPacket(client_intention_packet::ClientIntentionPacket),
+}
+
+#[async_trait]
+impl ProtocolPacket for HandshakePacket {
+ fn id(&self) -> u32 {
+ match self {
+ HandshakePacket::ClientIntentionPacket(_packet) => 0x00,
+ }
+ }
+
+ fn write(&self, buf: &mut Vec<u8>) {
+ match self {
+ HandshakePacket::ClientIntentionPacket(packet) => packet.write(buf),
+ }
+ }
+
+ /// Read a packet by its id, ConnectionProtocol, and flow
+ async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ id: u32,
+ flow: &PacketFlow,
+ buf: &mut BufReader<T>,
+ ) -> Result<HandshakePacket, String>
+ where
+ Self: Sized,
+ {
+ match flow {
+ PacketFlow::ServerToClient => Err("HandshakePacket::read not implemented".to_string()),
+ PacketFlow::ClientToServer => match id {
+ 0x00 => Ok(client_intention_packet::ClientIntentionPacket::read(buf).await?),
+ _ => Err(format!("Unknown ClientToServer status packet id: {}", id)),
+ },
+ }
+ }
+}
diff --git a/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs b/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs
new file mode 100644
index 00000000..093176eb
--- /dev/null
+++ b/azalea-protocol/src/packets/login/clientbound_custom_query_packet.rs
@@ -0,0 +1,41 @@
+use std::hash::Hash;
+use tokio::io::BufReader;
+
+use crate::mc_buf::{self, Readable, Writable};
+
+use super::LoginPacket;
+
+#[derive(Hash, Clone, Debug)]
+pub struct ClientboundCustomQueryPacket {
+ pub transaction_id: u32,
+ // TODO: this should be a resource location
+ pub identifier: String,
+ pub data: Vec<u8>,
+}
+
+impl ClientboundCustomQueryPacket {
+ pub fn get(self) -> LoginPacket {
+ LoginPacket::ClientboundCustomQueryPacket(self)
+ }
+
+ pub fn write(&self, buf: &mut Vec<u8>) {
+ buf.write_varint(self.transaction_id as i32).unwrap();
+ buf.write_utf(&self.identifier).unwrap();
+ buf.write_bytes(&self.data).unwrap();
+ }
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ buf: &mut BufReader<T>,
+ ) -> Result<LoginPacket, String> {
+ let transaction_id = buf.read_varint().await?.0 as u32;
+ // TODO: this should be a resource location
+ let identifier = buf.read_utf().await?;
+ let data = buf.read_bytes(1048576).await?;
+ Ok(ClientboundCustomQueryPacket {
+ transaction_id,
+ identifier,
+ data,
+ }
+ .get())
+ }
+}
diff --git a/azalea-protocol/src/packets/login/clientbound_hello_packet.rs b/azalea-protocol/src/packets/login/clientbound_hello_packet.rs
new file mode 100644
index 00000000..36a48706
--- /dev/null
+++ b/azalea-protocol/src/packets/login/clientbound_hello_packet.rs
@@ -0,0 +1,38 @@
+use std::hash::Hash;
+use tokio::io::BufReader;
+
+use crate::mc_buf::Readable;
+
+use super::LoginPacket;
+
+#[derive(Hash, Clone, Debug)]
+pub struct ClientboundHelloPacket {
+ pub server_id: String,
+ pub public_key: Vec<u8>,
+ pub nonce: Vec<u8>,
+}
+
+impl ClientboundHelloPacket {
+ pub fn get(self) -> LoginPacket {
+ LoginPacket::ClientboundHelloPacket(self)
+ }
+
+ pub fn write(&self, _buf: &mut Vec<u8>) {
+ panic!("ClientboundHelloPacket::write not implemented")
+ }
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ buf: &mut BufReader<T>,
+ ) -> Result<LoginPacket, String> {
+ let server_id = buf.read_utf_with_len(20).await?;
+ let public_key = buf.read_byte_array().await?;
+ let nonce = buf.read_byte_array().await?;
+
+ Ok(ClientboundHelloPacket {
+ server_id,
+ public_key,
+ nonce,
+ }
+ .get())
+ }
+}
diff --git a/azalea-protocol/src/packets/login/mod.rs b/azalea-protocol/src/packets/login/mod.rs
new file mode 100644
index 00000000..f0ed6717
--- /dev/null
+++ b/azalea-protocol/src/packets/login/mod.rs
@@ -0,0 +1,63 @@
+pub mod clientbound_custom_query_packet;
+pub mod clientbound_hello_packet;
+pub mod serverbound_hello_packet;
+
+use async_trait::async_trait;
+use tokio::io::BufReader;
+
+use crate::connect::PacketFlow;
+
+use super::ProtocolPacket;
+
+#[derive(Clone, Debug)]
+pub enum LoginPacket
+where
+ Self: Sized,
+{
+ ClientboundCustomQueryPacket(clientbound_custom_query_packet::ClientboundCustomQueryPacket),
+ ServerboundHelloPacket(serverbound_hello_packet::ServerboundHelloPacket),
+ ClientboundHelloPacket(clientbound_hello_packet::ClientboundHelloPacket),
+}
+
+#[async_trait]
+impl ProtocolPacket for LoginPacket {
+ fn id(&self) -> u32 {
+ match self {
+ LoginPacket::ClientboundCustomQueryPacket(_packet) => 0x04,
+ LoginPacket::ServerboundHelloPacket(_packet) => 0x00,
+ LoginPacket::ClientboundHelloPacket(_packet) => 0x01,
+ }
+ }
+
+ fn write(&self, buf: &mut Vec<u8>) {
+ match self {
+ LoginPacket::ClientboundCustomQueryPacket(packet) => packet.write(buf),
+ LoginPacket::ServerboundHelloPacket(packet) => packet.write(buf),
+ LoginPacket::ClientboundHelloPacket(packet) => packet.write(buf),
+ }
+ }
+
+ /// Read a packet by its id, ConnectionProtocol, and flow
+ async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ id: u32,
+ flow: &PacketFlow,
+ buf: &mut BufReader<T>,
+ ) -> Result<LoginPacket, String>
+ where
+ Self: Sized,
+ {
+ Ok(match flow {
+ PacketFlow::ServerToClient => match id {
+ 0x01 => clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?,
+ 0x04 => {
+ clientbound_custom_query_packet::ClientboundCustomQueryPacket::read(buf).await?
+ }
+ _ => return Err(format!("Unknown ServerToClient status packet id: {}", id)),
+ },
+ PacketFlow::ClientToServer => match id {
+ 0x00 => serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?,
+ _ => return Err(format!("Unknown ClientToServer status packet id: {}", id)),
+ },
+ })
+ }
+}
diff --git a/azalea-protocol/src/packets/login/serverbound_hello_packet.rs b/azalea-protocol/src/packets/login/serverbound_hello_packet.rs
new file mode 100644
index 00000000..32a6dadc
--- /dev/null
+++ b/azalea-protocol/src/packets/login/serverbound_hello_packet.rs
@@ -0,0 +1,27 @@
+use std::hash::Hash;
+use tokio::io::BufReader;
+
+use crate::mc_buf::Writable;
+
+use super::LoginPacket;
+
+#[derive(Hash, Clone, Debug)]
+pub struct ServerboundHelloPacket {
+ pub username: String,
+}
+
+impl ServerboundHelloPacket {
+ pub fn get(self) -> LoginPacket {
+ LoginPacket::ServerboundHelloPacket(self)
+ }
+
+ pub fn write(&self, buf: &mut Vec<u8>) {
+ buf.write_utf(&self.username).unwrap();
+ }
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ _buf: &mut BufReader<T>,
+ ) -> Result<LoginPacket, String> {
+ Err("ServerboundHelloPacket::read not implemented".to_string())
+ }
+}
diff --git a/azalea-protocol/src/packets/mod.rs b/azalea-protocol/src/packets/mod.rs
new file mode 100644
index 00000000..a074b570
--- /dev/null
+++ b/azalea-protocol/src/packets/mod.rs
@@ -0,0 +1,146 @@
+pub mod game;
+pub mod handshake;
+pub mod login;
+pub mod status;
+
+use async_trait::async_trait;
+use tokio::io::BufReader;
+
+use crate::connect::PacketFlow;
+
+pub const PROTOCOL_VERSION: u32 = 757;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ConnectionProtocol {
+ Handshake = -1,
+ Game = 0,
+ Status = 1,
+ Login = 2,
+}
+
+#[derive(Clone, Debug)]
+pub enum Packet {
+ Game(game::GamePacket),
+ Handshake(handshake::HandshakePacket),
+ Login(login::LoginPacket),
+ Status(Box<status::StatusPacket>),
+}
+
+/// An enum of packets for a certain protocol
+#[async_trait]
+pub trait ProtocolPacket
+where
+ Self: Sized,
+{
+ fn id(&self) -> u32;
+
+ /// Read a packet by its id, ConnectionProtocol, and flow
+ async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ id: u32,
+ flow: &PacketFlow,
+ buf: &mut BufReader<T>,
+ ) -> Result<Self, String>
+ where
+ Self: Sized;
+
+ fn write(&self, buf: &mut Vec<u8>);
+}
+
+// impl Packet {
+// fn get_inner_packet(&self) -> &dyn PacketTrait {
+// match self {
+// Packet::ClientIntentionPacket(packet) => packet,
+// Packet::ServerboundStatusRequestPacket(packet) => packet,
+// Packet::ClientboundStatusResponsePacket(packet) => packet,
+// Packet::ServerboundHelloPacket(packet) => packet,
+// Packet::ClientboundHelloPacket(packet) => packet,
+// }
+// }
+
+// pub fn id(&self) -> u32 {
+// match self {
+// Packet::ClientIntentionPacket(_packet) => 0x00,
+// Packet::ServerboundStatusRequestPacket(_packet) => 0x00,
+// Packet::ClientboundStatusResponsePacket(_packet) => 0x00,
+// Packet::ServerboundHelloPacket(_packet) => 0x00,
+// Packet::ClientboundHelloPacket(_packet) => 0x01,
+// }
+// }
+
+// /// Read a packet by its id, ConnectionProtocol, and flow
+// pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+// id: u32,
+// protocol: &ConnectionProtocol,
+// flow: &PacketFlow,
+// buf: &mut BufReader<T>,
+// ) -> Result<Packet, String> {
+// match protocol {
+// ConnectionProtocol::Handshake => match flow {
+// PacketFlow::ClientToServer => match id {
+// 0x00 => Ok(
+// handshake::client_intention_packet::ClientIntentionPacket::read(buf).await?,
+// ),
+// _ => Err(format!("Unknown ClientToServer handshake packet id: {}", id)),
+// }
+// PacketFlow::ServerToClient => Err("ServerToClient handshake packets not implemented".to_string()),
+// },
+
+// ConnectionProtocol::Game => Err("Game protocol not implemented yet".to_string()),
+
+// ConnectionProtocol::Status => match flow {
+// PacketFlow::ServerToClient => match id {
+// 0x00 => Ok(
+// status::clientbound_status_response_packet::ClientboundStatusResponsePacket
+// ::read(buf)
+// .await?,
+// ),
+// _ => Err(format!("Unknown ServerToClient status packet id: {}", id)),
+// },
+// PacketFlow::ClientToServer => match id {
+// 0x00 => Ok(
+// status::serverbound_status_request_packet::ServerboundStatusRequestPacket
+// ::read(buf)
+// .await?,
+// ),
+// _ => Err(format!("Unknown ClientToServer status packet id: {}", id)),
+// },
+// },
+
+// ConnectionProtocol::Login => match flow {
+// PacketFlow::ServerToClient => match id {
+// 0x01 => Ok(
+// login::clientbound_hello_packet::ClientboundHelloPacket::read(buf).await?,
+// ),
+// _ => Err(format!("Unknown ServerToClient login packet id: {}", id)),
+// },
+// PacketFlow::ClientToServer => match id {
+// 0x00 => Ok(
+// login::serverbound_hello_packet::ServerboundHelloPacket::read(buf).await?,
+// ),
+// _ => Err(format!("Unknown ClientToServer login packet id: {}", id)),
+// },
+// },
+// }
+// }
+
+// pub fn write(&self, buf: &mut Vec<u8>) {
+// self.get_inner_packet().write(buf);
+// }
+// }
+
+// #[async_trait]
+// pub trait PacketTrait
+// where
+// Self: Sized,
+// {
+// /// Return a version of the packet that you can actually use for stuff
+// fn get(self) -> dyn ProtocolPacket;
+
+// fn write(&self, buf: &mut Vec<u8>);
+
+// async fn read<T: AsyncRead + std::marker::Unpin + std::marker::Send, P: ProtocolPacket>(
+// buf: &mut BufReader<T>,
+// ) -> Result<P, String>
+// where
+// Self: Sized;
+// }
diff --git a/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs b/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs
new file mode 100644
index 00000000..1d8a3aa4
--- /dev/null
+++ b/azalea-protocol/src/packets/status/clientbound_status_response_packet.rs
@@ -0,0 +1,58 @@
+use azalea_chat::component::Component;
+use serde::Deserialize;
+use serde_json::Value;
+use tokio::io::BufReader;
+
+use crate::mc_buf::Readable;
+
+use super::StatusPacket;
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Version {
+ pub name: Component,
+ pub protocol: u32,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct SamplePlayer {
+ pub id: String,
+ pub name: String,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Players {
+ pub max: u32,
+ pub online: u32,
+ pub sample: Vec<SamplePlayer>,
+}
+
+// the entire packet is just json, which is why it has deserialize
+#[derive(Clone, Debug, Deserialize)]
+pub struct ClientboundStatusResponsePacket {
+ pub description: Component,
+ pub favicon: Option<String>,
+ pub players: Players,
+ pub version: Version,
+}
+
+impl ClientboundStatusResponsePacket {
+ pub fn get(self) -> StatusPacket {
+ StatusPacket::ClientboundStatusResponsePacket(Box::new(self))
+ }
+
+ pub fn write(&self, _buf: &mut Vec<u8>) {}
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ buf: &mut BufReader<T>,
+ ) -> Result<StatusPacket, String> {
+ let status_string = buf.read_utf().await?;
+ let status_json: Value =
+ serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON");
+
+ let packet = ClientboundStatusResponsePacket::deserialize(status_json)
+ .map_err(|e| e.to_string())?
+ .get();
+
+ Ok(packet)
+ }
+}
diff --git a/azalea-protocol/src/packets/status/mod.rs b/azalea-protocol/src/packets/status/mod.rs
new file mode 100644
index 00000000..ac6a34e1
--- /dev/null
+++ b/azalea-protocol/src/packets/status/mod.rs
@@ -0,0 +1,66 @@
+pub mod clientbound_status_response_packet;
+pub mod serverbound_status_request_packet;
+
+use async_trait::async_trait;
+use tokio::io::BufReader;
+
+use crate::connect::PacketFlow;
+
+use super::ProtocolPacket;
+
+#[derive(Clone, Debug)]
+pub enum StatusPacket
+where
+ Self: Sized,
+{
+ ServerboundStatusRequestPacket(
+ serverbound_status_request_packet::ServerboundStatusRequestPacket,
+ ),
+ ClientboundStatusResponsePacket(
+ Box<clientbound_status_response_packet::ClientboundStatusResponsePacket>,
+ ),
+}
+
+#[async_trait]
+impl ProtocolPacket for StatusPacket {
+ fn id(&self) -> u32 {
+ match self {
+ StatusPacket::ServerboundStatusRequestPacket(_packet) => 0x00,
+ StatusPacket::ClientboundStatusResponsePacket(_packet) => 0x00,
+ }
+ }
+
+ fn write(&self, buf: &mut Vec<u8>) {
+ match self {
+ StatusPacket::ServerboundStatusRequestPacket(packet) => packet.write(buf),
+ StatusPacket::ClientboundStatusResponsePacket(packet) => packet.write(buf),
+ }
+ }
+
+ /// Read a packet by its id, ConnectionProtocol, and flow
+ async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ id: u32,
+ flow: &PacketFlow,
+ buf: &mut BufReader<T>,
+ ) -> Result<StatusPacket, String>
+ where
+ Self: Sized,
+ {
+ match flow {
+ PacketFlow::ServerToClient => match id {
+ 0x00 => Ok(
+ clientbound_status_response_packet::ClientboundStatusResponsePacket::read(buf)
+ .await?,
+ ),
+ _ => Err(format!("Unknown ServerToClient status packet id: {}", id)),
+ },
+ PacketFlow::ClientToServer => match id {
+ 0x00 => Ok(
+ serverbound_status_request_packet::ServerboundStatusRequestPacket::read(buf)
+ .await?,
+ ),
+ _ => Err(format!("Unknown ClientToServer status packet id: {}", id)),
+ },
+ }
+ }
+}
diff --git a/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs b/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs
new file mode 100644
index 00000000..6a58da1f
--- /dev/null
+++ b/azalea-protocol/src/packets/status/serverbound_status_request_packet.rs
@@ -0,0 +1,23 @@
+use std::hash::Hash;
+use tokio::io::BufReader;
+
+use super::StatusPacket;
+
+#[derive(Hash, Clone, Debug)]
+pub struct ServerboundStatusRequestPacket {}
+
+impl ServerboundStatusRequestPacket {
+ pub fn get(self) -> StatusPacket {
+ StatusPacket::ServerboundStatusRequestPacket(self)
+ }
+
+ pub fn write(&self, _buf: &mut Vec<u8>) {
+ panic!("ServerboundStatusRequestPacket::write not implemented")
+ }
+
+ pub async fn read<T: tokio::io::AsyncRead + std::marker::Unpin + std::marker::Send>(
+ _buf: &mut BufReader<T>,
+ ) -> Result<StatusPacket, String> {
+ Err("ServerboundStatusRequestPacket::read not implemented".to_string())
+ }
+}
diff --git a/azalea-protocol/src/read.rs b/azalea-protocol/src/read.rs
new file mode 100644
index 00000000..6f242e8b
--- /dev/null
+++ b/azalea-protocol/src/read.rs
@@ -0,0 +1,28 @@
+use tokio::{io::BufReader, net::TcpStream};
+
+use crate::{connect::PacketFlow, mc_buf::Readable, packets::ProtocolPacket};
+
+pub async fn read_packet<P: ProtocolPacket>(
+ flow: &PacketFlow,
+ stream: &mut TcpStream,
+) -> Result<P, String> {
+ // what this does:
+ // 1. reads the first 5 bytes, probably only some of this will be used to get the packet length
+ // 2. how much we should read = packet length - 5
+ // 3. read the rest of the packet and add it to the cursor
+ // 4. figure out what packet this is and parse it
+
+ // the first thing minecraft sends us is the length as a varint, which can be up to 5 bytes long
+ let mut buf = BufReader::with_capacity(4 * 1024 * 1024, stream);
+
+ let (_packet_size, _packet_size_varint_size) = buf.read_varint().await?;
+
+ // then, minecraft tells us the packet id as a varint
+ let (packet_id, _packet_id_size) = buf.read_varint().await?;
+
+ // if we recognize the packet id, parse it
+
+ let packet = P::read(packet_id.try_into().unwrap(), flow, &mut buf).await?;
+
+ Ok(packet)
+}
diff --git a/azalea-protocol/src/resolver.rs b/azalea-protocol/src/resolver.rs
new file mode 100644
index 00000000..24687a6e
--- /dev/null
+++ b/azalea-protocol/src/resolver.rs
@@ -0,0 +1,55 @@
+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,
+ 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 let Ok(redirect_result) = srv_redirect_result {
+ 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/azalea-protocol/src/write.rs b/azalea-protocol/src/write.rs
new file mode 100644
index 00000000..3d8540eb
--- /dev/null
+++ b/azalea-protocol/src/write.rs
@@ -0,0 +1,27 @@
+use tokio::{io::AsyncWriteExt, net::TcpStream};
+
+use crate::{mc_buf::Writable, packets::ProtocolPacket};
+
+pub async fn write_packet(packet: impl ProtocolPacket, stream: &mut TcpStream) {
+ // TODO: implement compression
+
+ // packet structure:
+ // length (varint) + id (varint) + data
+
+ // write the packet id
+ let mut id_and_data_buf = vec![];
+ id_and_data_buf.write_varint(packet.id() as i32);
+ packet.write(&mut id_and_data_buf);
+
+ // write the packet data
+
+ // make a new buffer that has the length at the beginning
+ // and id+data at the end
+ let mut complete_buf: Vec<u8> = Vec::new();
+ complete_buf.write_varint(id_and_data_buf.len() as i32);
+ complete_buf.append(&mut id_and_data_buf);
+
+ // finally, write and flush to the stream
+ stream.write_all(&complete_buf).await.unwrap();
+ stream.flush().await.unwrap();
+}