diff --git a/Cargo.lock b/Cargo.lock index 0dfe103..5b251e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,19 @@ version = 4 [[package]] -name = "client" +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "mc_chat_client" version = "0.1.0" dependencies = [ "serde_json", "varint-rs", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index 15378af..7897e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "client" +name = "mc_chat_client" version = "0.1.0" edition = "2024" diff --git a/src/connect.rs b/src/connect.rs new file mode 100644 index 0000000..e95468a --- /dev/null +++ b/src/connect.rs @@ -0,0 +1,246 @@ +use std::{ + collections::HashMap, + io::{Read, Write}, +}; + +use varint_rs::{VarintReader, VarintWriter}; + +use crate::{ + Connected, Server, Status, + error::ChatError, + error::Result, + readers::{read_packet, read_plugin_msg, read_string}, + status::ServerType, + writers::{write_packet, write_plugin_msg, write_string}, +}; + +impl Server { + pub fn connect(self) -> Result> { + let Self { + mut stream, + addr, + port, + state: + Status { + typ, + description, + mods, + }, + } = self; + + let (uuid, username) = match typ { + ServerType::Vanilla => { + // Handshake + write_packet(&mut stream, |buf| { + buf.write_u8_varint(0)?; + buf.write_u8_varint(5)?; + buf.write_usize_varint(addr.len())?; + buf.write_all(addr.as_bytes())?; + buf.write_all(&port.to_le_bytes())?; + buf.write_u8_varint(2)?; + Ok(()) + })?; + + // Login start + write_packet(&mut stream, |buf| { + buf.write_u8_varint(0)?; + let uname = "McChatClient"; + write_string(buf, uname)?; + Ok(()) + })?; + + read_packet(&mut stream, |packet| { + let id = packet.read_usize_varint()?; + assert!(id == 2); + let uuid = read_string(packet)?; + let username = read_string(packet)?; + Ok((uuid, username)) + })? + } + ServerType::Forge => { + // Handshake + + write_packet(&mut stream, |buf| { + buf.write_u8_varint(0)?; + buf.write_u8_varint(5)?; + let addr = format!("{addr}\0FML\0"); + buf.write_usize_varint(addr.len())?; + buf.write_all(addr.as_bytes())?; + buf.write_all(&port.to_le_bytes())?; + buf.write_u8_varint(2)?; + Ok(()) + })?; + + // Login start + write_packet(&mut stream, |buf| { + buf.write_u8_varint(0)?; + let uname = "McChatClient"; + write_string(buf, uname)?; + Ok(()) + })?; + + // Login Success + let (uuid, username) = read_packet(&mut stream, |packet| { + let id = packet.read_usize_varint()?; + assert!(id == 2); + let uuid = read_string(packet)?; + let username = read_string(packet)?; + Ok((uuid, username)) + })?; + + // Server Register Channels + let (_channels, channels_buf) = + read_plugin_msg(&mut stream, "REGISTER", |packet| { + Ok(( + packet + .split(|b| *b == 0) + .map(|b| String::from_utf8(b.to_vec()).map_err(ChatError::from)) + .collect::>>()?, + packet.to_vec(), + )) + })?; + + // Server Hello + let _dim = read_plugin_msg(&mut stream, "FML|HS", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 0); + let proto_ver = VarintReader::read(packet)?; + assert_eq!(proto_ver, 2); + let mut buf = [0u8; 4]; + packet.read_exact(&mut buf)?; + let dim = i32::from_be_bytes(buf); + Ok(dim) + })?; + + // Client Register Channels + write_plugin_msg(&mut stream, "REGISTER", |body| { + body.write_all(&channels_buf)?; + Ok(()) + })?; + + // Client Hello + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0x1])?; + body.write_all(&[0x2])?; + Ok(()) + })?; + + // Client Modlist + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0x2])?; + let mods = mods.as_ref().unwrap(); + body.write_usize_varint(mods.len())?; + + for (id, ver) in mods { + write_string(body, id)?; + write_string(body, ver)?; + } + Ok(()) + })?; + + // Server Modlist + let _mods = read_plugin_msg(&mut stream, "FML|HS", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 2); + let num_mods = VarintReader::read(packet)?; + + let mut mods = HashMap::new(); + for _ in 0..num_mods { + let id = read_string(packet)?; + let ver = read_string(packet)?; + mods.insert(id, ver); + } + Ok(mods) + })?; + + // HandshakeAck waitingseverdata + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0xFF, 0x02])?; + Ok(()) + })?; + + // Server RegistryData + read_plugin_msg(&mut stream, "FML|HS", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 3); + let num_ids = packet.read_usize_varint()?; + + for _ in 0..num_ids { + let _name = read_string(packet)?; + let _id = packet.read_usize_varint()?; + } + + if packet.len() > 0 { + let len = packet.read_usize_varint()?; + for _ in 0..len { + read_string(packet)?; + } + + let len = packet.read_usize_varint()?; + for _ in 0..len { + read_string(packet)?; + } + } + Ok(()) + })?; + + // Server HandshakeAck waitingack + read_plugin_msg(&mut stream, "FML|HS", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 255); + let phrase = VarintReader::read(packet)?; + assert_eq!(phrase, 2); + Ok(()) + })?; + + // HandshakeAck waitingsevercomplete + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0xFF, 0x03])?; + Ok(()) + })?; + + // Serrver FluidIdmap + read_plugin_msg(&mut stream, "FORGE", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 2); + Ok(()) + })?; + + // HandshakeAck pendingcomplete + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0xFF, 0x04])?; + Ok(()) + })?; + + // Server HandshakeAck complete + read_plugin_msg(&mut stream, "FML|HS", |packet| { + let discriminant = VarintReader::read(packet)?; + assert_eq!(discriminant, 255); + let phrase = VarintReader::read(packet)?; + assert_eq!(phrase, 3); + Ok(()) + })?; + + // HandshakeAck complete + write_plugin_msg(&mut stream, "FML|HS", |body| { + body.write_all(&[0xFF, 0x05])?; + Ok(()) + })?; + + (uuid, username) + } + }; + + Ok(Server { + stream, + addr, + port, + state: Connected { + typ, + description, + uuid, + username, + }, + }) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5ca51ea --- /dev/null +++ b/src/error.rs @@ -0,0 +1,44 @@ +use serde_json::Value; +use std::{io, result::Result as StdResult, string::FromUtf8Error}; + +#[derive(Debug)] +pub enum ChatError { + IoError(io::Error), + FromUtf8Error(FromUtf8Error), + JsonError(JsonError), +} + +#[derive(Debug)] +pub enum JsonError { + SerdeError(serde_json::Error), + ExpectedObject(Value), + ExpectedArray, + ExpectedString, + ExpectedMember(String), +} + +impl From for ChatError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for ChatError { + fn from(value: FromUtf8Error) -> Self { + Self::FromUtf8Error(value) + } +} + +impl From for ChatError { + fn from(value: JsonError) -> Self { + Self::JsonError(value) + } +} + +impl From for JsonError { + fn from(value: serde_json::Error) -> Self { + Self::SerdeError(value) + } +} + +pub type Result = StdResult; diff --git a/src/main.rs b/src/main.rs index 61bec41..d0ee982 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,63 +1,75 @@ +mod connect; +mod error; mod readers; +mod status; mod text; +mod writers; use std::{ + collections::HashMap, + fmt::Debug, io::{Read, Write}, net::TcpStream, }; -use readers::{read_packet, read_string}; +use error::Result; +use readers::read_string; +use status::ServerType; use text::Message; use varint_rs::{VarintReader, VarintWriter}; -fn main() { - let mut stream = TcpStream::connect("localhost:25565").unwrap(); +pub trait ServerState {} - // Handshake - let mut buf: Vec = Vec::new(); - buf.write_u8_varint(0).unwrap(); - buf.write_u8_varint(5).unwrap(); - let addr = "localhost"; - buf.write_usize_varint(addr.len()).unwrap(); - buf.write_all(addr.as_bytes()).unwrap(); - buf.write_all(&25565u16.to_le_bytes()).unwrap(); - buf.write_u8_varint(2).unwrap(); +#[derive(Debug)] +pub struct Disconnected; +impl ServerState for Disconnected {} +#[derive(Debug)] +pub struct Status { + typ: ServerType, + description: String, + mods: Option>, +} +impl ServerState for Status {} +#[derive(Debug)] +#[allow(unused)] +pub struct Connected { + typ: ServerType, + description: String, + uuid: String, + username: String, +} +impl ServerState for Connected {} - stream.write_usize_varint(buf.len()).unwrap(); - stream.write_all(&buf).unwrap(); +#[derive(Debug)] +pub struct Server { + stream: TcpStream, + addr: String, + port: u16, + state: S, +} - // Login start - let mut buf: Vec = Vec::new(); - buf.write_u8_varint(0).unwrap(); - let addr = "CursedChat"; - buf.write_usize_varint(addr.len()).unwrap(); - buf.write_all(addr.as_bytes()).unwrap(); +impl Server { + fn new(addr: String, port: u16) -> Result { + Ok(Self { + stream: TcpStream::connect(format!("{addr}:{port}"))?, + addr, + port, + state: Disconnected, + }) + } +} - stream.write_usize_varint(buf.len()).unwrap(); - stream.write_all(&buf).unwrap(); +fn main() -> Result<()> { + let server = Server::new("localhost".to_string(), 25565)?; + dbg!(&server); - let mut buf = [0u8; 128]; - dbg!(Read::read(&mut stream, &mut buf).unwrap()); - println!("{:X?}", buf); + let server = server.status()?; + dbg!(&server); - let mut iter = buf.into_iter(); - let _len = iter.next().unwrap(); - let id = iter.next().unwrap(); - assert!(id == 2); - let uuid = read_string(&mut iter); - let username = read_string(&mut iter); - dbg!(uuid, username); + let server = server.connect()?; + dbg!(&server); - loop { - let packet = read_packet(&mut stream); - let packetid = (&packet[..]).read_usize_varint().unwrap(); - match packetid { - 0x00 => handle_keepalive(&mut stream, &packet), - 0x02 => handle_chat(&packet), - 0x40 => break, - _ => {} - } - } + Ok(()) } fn handle_keepalive(stream: &mut TcpStream, mut packet: &[u8]) { @@ -76,8 +88,7 @@ fn handle_keepalive(stream: &mut TcpStream, mut packet: &[u8]) { fn handle_chat(mut packet: &[u8]) { let _packetid = packet.read_usize_varint().unwrap(); - let mut iter = packet.iter().cloned(); - let msg = read_string(&mut iter); + let msg = read_string(&mut packet); let msg = Message::from_string(msg.unwrap()); dbg!(msg); } diff --git a/src/readers.rs b/src/readers.rs index 07cc420..ddb3f8a 100644 --- a/src/readers.rs +++ b/src/readers.rs @@ -1,19 +1,47 @@ -use std::{io::Read as _, net::TcpStream}; +use std::{io::Read, net::TcpStream}; + +use crate::error::Result; use varint_rs::VarintReader as _; -pub fn read_string>(iter: &mut I) -> Option { - let len = iter.next()?.into(); - let bytes = iter.take(len).collect::>(); - (len == bytes.len()).then_some(())?; - String::from_utf8(bytes).ok() +pub fn read_string(iter: &mut &[u8]) -> Result { + let len = iter.read_usize_varint()?; + let mut buf = vec![0u8; len]; + iter.read_exact(&mut buf)?; + String::from_utf8(buf).map_err(Into::into) } -pub fn read_packet(stream: &mut TcpStream) -> Vec { - let len = stream.read_usize_varint().unwrap(); +pub fn read_packet Result>( + stream: &mut TcpStream, + read_body: F, +) -> Result { + let len = stream.read_usize_varint()?; let mut buf = vec![0u8; len]; - stream.read_exact(&mut buf).unwrap(); + stream.read_exact(&mut buf)?; + + let mut packet = &buf[..]; + read_body(&mut packet) +} + +pub fn read_plugin_msg Result>( + stream: &mut TcpStream, + channel: &str, + read_body: F, +) -> Result { + read_packet(stream, |packet| { + let id = packet.read_usize_varint()?; + assert!(id == 63); + + let channel_read = read_string(packet)?; + assert_eq!(channel_read, channel); + let mut buf = [0u8; 2]; + packet.read_exact(&mut buf)?; + let len = u16::from_be_bytes(buf); + let mut buf = vec![0u8; len as usize]; + packet.read_exact(&mut buf)?; - buf + let mut packet = &buf[..]; + read_body(&mut packet) + }) } diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..52ace52 --- /dev/null +++ b/src/status.rs @@ -0,0 +1,118 @@ +use std::{collections::HashMap, io::Write, net::TcpStream}; + +use serde_json::Value; +use varint_rs::{VarintReader, VarintWriter}; + +use crate::{ + Disconnected, Server, Status, + error::ChatError, + error::JsonError, + error::Result, + readers::{read_packet, read_string}, + writers::write_packet, +}; + +#[derive(Debug)] +pub enum ServerType { + Vanilla, + Forge, +} + +impl Server { + pub fn status(self) -> Result> { + let Self { + mut stream, + addr, + port, + .. + } = self; + + // Handshake + + write_packet(&mut stream, |buf| { + buf.write_u8_varint(0)?; + buf.write_u8_varint(5)?; + buf.write_usize_varint(addr.len())?; + buf.write_all(addr.as_bytes())?; + buf.write_all(&port.to_le_bytes())?; + buf.write_u8_varint(1)?; + Ok(()) + })?; + + write_packet(&mut stream, |buf| { + buf.write_all(&[0x0])?; + Ok(()) + })?; + + let json: Value = read_packet(&mut stream, |packet| { + let id = packet.read_usize_varint()?; + assert!(id == 0); + let json = read_string(packet)?; + serde_json::from_str(&json) + .map_err(>::from) + .map_err(ChatError::from) + })?; + + let json = json + .as_object() + .ok_or(JsonError::ExpectedObject(json.clone()))?; + let description = json + .get("description") + .ok_or(JsonError::ExpectedMember("description".to_string()))? + .as_str() + .ok_or(JsonError::ExpectedString)? + .to_string(); + + let (typ, mods) = match json.get("modinfo") { + Some(info) => { + if info + .as_object() + .ok_or(JsonError::ExpectedObject(info.clone()))? + .get("type") + .ok_or(JsonError::ExpectedMember("type".to_string()))? + .as_str() + .ok_or(JsonError::ExpectedString)? + == "FML" + { + let mod_list = info + .get("modList") + .ok_or(JsonError::ExpectedMember("modList".to_string()))?; + let mod_list = mod_list.as_array().ok_or(JsonError::ExpectedArray)?; + let mut mods = HashMap::new(); + for mod_ in mod_list { + let mod_ = mod_ + .as_object() + .ok_or(JsonError::ExpectedObject(mod_.clone()))?; + let id = mod_ + .get("modid") + .ok_or(JsonError::ExpectedMember("modid".to_string()))?; + let ver = mod_ + .get("version") + .ok_or(JsonError::ExpectedMember("version".to_string()))?; + mods.insert( + id.as_str().ok_or(JsonError::ExpectedString)?.to_string(), + ver.as_str().ok_or(JsonError::ExpectedString)?.to_string(), + ); + } + (ServerType::Forge, Some(mods)) + } else { + panic!() + } + } + None => (ServerType::Vanilla, None), + }; + + stream.shutdown(std::net::Shutdown::Both)?; + + Ok(Server { + stream: TcpStream::connect(format!("{addr}:{port}"))?, + addr, + port, + state: Status { + typ, + description, + mods, + }, + }) + } +} diff --git a/src/writers.rs b/src/writers.rs new file mode 100644 index 0000000..e9da5bc --- /dev/null +++ b/src/writers.rs @@ -0,0 +1,41 @@ +use std::{io::Write, net::TcpStream}; + +use varint_rs::VarintWriter; + +use crate::error::Result; + +pub fn write_packet) -> Result<()>>( + stream: &mut TcpStream, + build_packet: F, +) -> Result<()> { + let mut buf: Vec = Vec::new(); + build_packet(&mut buf)?; + + stream.write_usize_varint(buf.len())?; + stream.write_all(&buf)?; + + Ok(()) +} + +pub fn write_string(buf: &mut W, val: &str) -> Result<()> { + buf.write_usize_varint(val.len())?; + buf.write_all(val.as_bytes())?; + Ok(()) +} + +pub fn write_plugin_msg) -> Result<()>>( + stream: &mut TcpStream, + channel: &str, + build_body: F, +) -> Result<()> { + let mut body: Vec = Vec::new(); + build_body(&mut body)?; + + write_packet(stream, |mut buf| { + buf.write_u8_varint(0x17)?; + write_string(&mut buf, channel)?; + buf.write_all(&(body.len() as u16).to_be_bytes())?; + buf.write_all(&body)?; + Ok(()) + }) +}