diff --git a/Cargo.toml b/Cargo.toml index d6af761..1476f81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ sha1 = "0.10" sha2 = "0.10" thiserror = "1" tokio = { version = "1", features = ["full"] } -url = {version = "2.2.2", features = ["serde"] } +url = { version = "2.2.2", features = ["serde"] } uuid = "1" vek = "0.15" @@ -55,9 +55,12 @@ anyhow = "1" heck = "0.4" proc-macro2 = "1" quote = "1" -serde = {version = "1", features = ["derive"]} +serde = { version = "1", features = ["derive"] } serde_json = "1" [features] # Exposes the raw protocol API protocol = [] + +[workspace] +members = ["packet-inspector"] diff --git a/packet-inspector/Cargo.toml b/packet-inspector/Cargo.toml new file mode 100644 index 0000000..e641387 --- /dev/null +++ b/packet-inspector/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "packet-inspector" +version = "0.1.0" +edition = "2021" +description = "A simple Minecraft proxy for inspecting packets." + +[dependencies] +valence = { path = "..", features = ["protocol"] } +clap = { version = "3.2.8", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +anyhow = "1" +chrono = "0.4.19" diff --git a/packet-inspector/src/main.rs b/packet-inspector/src/main.rs new file mode 100644 index 0000000..7066f4d --- /dev/null +++ b/packet-inspector/src/main.rs @@ -0,0 +1,197 @@ +use std::error::Error; +use std::fmt; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use anyhow::bail; +use chrono::{Utc, DateTime}; +use clap::Parser; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::Semaphore; +use valence::protocol::codec::{Decoder, Encoder}; +use valence::protocol::packets::handshake::{Handshake, HandshakeNextState}; +use valence::protocol::packets::login::c2s::{EncryptionResponse, LoginStart}; +use valence::protocol::packets::login::s2c::{LoginSuccess, S2cLoginPacket}; +use valence::protocol::packets::play::c2s::C2sPlayPacket; +use valence::protocol::packets::play::s2c::S2cPlayPacket; +use valence::protocol::packets::status::c2s::{PingRequest, StatusRequest}; +use valence::protocol::packets::status::s2c::{PongResponse, StatusResponse}; +use valence::protocol::packets::{DecodePacket, EncodePacket}; + +#[derive(Parser, Clone, Debug)] +#[clap(author, version, about)] +struct Cli { + /// The socket address to listen for connections on. This is the address + /// clients should connect to. + client: SocketAddr, + /// The socket address the proxy will connect to. + server: SocketAddr, + + /// The maximum number of connections allowed to the proxy. By default, + /// there is no limit. + #[clap(short, long)] + max_connections: Option, + + /// When enabled, prints a timestamp before each packet. + #[clap(short, long)] + timestamp: bool, +} + +impl Cli { + fn print(&self, d: &impl fmt::Debug) { + if self.timestamp { + let now: DateTime = Utc::now(); + println!("{now} {d:?}"); + } else { + println!("{d:?}"); + } + } + + async fn rw_packet( + &self, + read: &mut Decoder, + write: &mut Encoder, + ) -> anyhow::Result

{ + let pkt = read.read_packet().await?; + self.print(&pkt); + write.write_packet(&pkt).await?; + Ok(pkt) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + let sema = Arc::new(Semaphore::new( + cli.max_connections.unwrap_or(usize::MAX).min(100_000), + )); + + eprintln!("Waiting for connections on {}", cli.client); + let listen = TcpListener::bind(cli.client).await?; + + while let Ok(permit) = sema.clone().acquire_owned().await { + let (client, remote_client_addr) = listen.accept().await?; + eprintln!("Accepted connection to {remote_client_addr}"); + + let cli = cli.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(client, cli).await { + eprintln!("Connection to {remote_client_addr} ended with: {e:#}"); + } else { + eprintln!("Connection to {remote_client_addr} ended."); + } + drop(permit); + }); + } + + Ok(()) +} + +async fn handle_connection(client: TcpStream, cli: Cli) -> anyhow::Result<()> { + eprintln!("Connecting to {}", cli.server); + + let server = TcpStream::connect(cli.server).await?; + + let (client_read, client_write) = client.into_split(); + let (server_read, server_write) = server.into_split(); + + let timeout = Duration::from_secs(10); + + let mut client_read = Decoder::new(client_read, timeout); + let mut client_write = Encoder::new(client_write, timeout); + + let mut server_read = Decoder::new(server_read, timeout); + let mut server_write = Encoder::new(server_write, timeout); + + let handshake: Handshake = cli.rw_packet(&mut client_read, &mut server_write).await?; + + match handshake.next_state { + HandshakeNextState::Status => { + cli.rw_packet::(&mut client_read, &mut server_write) + .await?; + cli.rw_packet::(&mut server_read, &mut client_write) + .await?; + + cli.rw_packet::(&mut client_read, &mut server_write) + .await?; + cli.rw_packet::(&mut server_read, &mut client_write) + .await?; + } + HandshakeNextState::Login => { + cli.rw_packet::(&mut client_read, &mut server_write) + .await?; + + match cli + .rw_packet::(&mut server_read, &mut client_write) + .await? + { + S2cLoginPacket::EncryptionRequest(_) => { + cli.rw_packet::(&mut client_read, &mut server_write) + .await?; + + eprintln!("Encryption was enabled! I can't see what's going on anymore."); + + return tokio::select! { + c2s = passthrough(client_read.into_inner(), server_write.into_inner()) => c2s, + s2c = passthrough(server_read.into_inner(), client_write.into_inner()) => s2c, + }; + } + S2cLoginPacket::SetCompression(pkt) => { + let threshold = pkt.threshold.0 as u32; + client_read.enable_compression(threshold); + client_write.enable_compression(threshold); + server_read.enable_compression(threshold); + server_write.enable_compression(threshold); + + cli.rw_packet::(&mut server_read, &mut client_write) + .await?; + } + S2cLoginPacket::LoginSuccess(_) => {} + S2cLoginPacket::Disconnect(_) => return Ok(()), + S2cLoginPacket::LoginPluginRequest(_) => { + bail!("got login plugin request. Don't know how to proceed.") + } + } + + let c2s = async { + loop { + cli.rw_packet::(&mut client_read, &mut server_write) + .await?; + } + }; + + let s2c = async { + loop { + cli.rw_packet::(&mut server_read, &mut client_write) + .await?; + } + }; + + return tokio::select! { + c2s = c2s => c2s, + s2c = s2c => s2c, + }; + } + } + + Ok(()) +} + +async fn passthrough(mut read: OwnedReadHalf, mut write: OwnedWriteHalf) -> anyhow::Result<()> { + let mut buf = vec![0u8; 4096].into_boxed_slice(); + loop { + let bytes_read = read.read(&mut buf).await?; + let bytes = &mut buf[..bytes_read]; + + if bytes.is_empty() { + break; + } + + write.write_all(bytes).await?; + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index d96208d..350b92a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub mod ident; mod player_list; pub mod player_textures; #[cfg(not(feature = "protocol"))] +#[allow(unused)] mod protocol; #[cfg(feature = "protocol")] pub mod protocol; diff --git a/src/protocol/codec.rs b/src/protocol/codec.rs index d6863f5..ee00e16 100644 --- a/src/protocol/codec.rs +++ b/src/protocol/codec.rs @@ -99,6 +99,10 @@ impl Encoder { pub fn enable_compression(&mut self, threshold: u32) { self.compression_threshold = Some(threshold); } + + pub fn into_inner(self) -> W { + self.write + } } pub struct Decoder { @@ -222,6 +226,10 @@ impl Decoder { pub fn enable_compression(&mut self, threshold: u32) { self.compression_threshold = Some(threshold); } + + pub fn into_inner(self) -> R { + self.read + } } /// The AES block cipher with a 128 bit key, using the CFB-8 mode of diff --git a/src/protocol/packets.rs b/src/protocol/packets.rs index 3055118..781f52a 100644 --- a/src/protocol/packets.rs +++ b/src/protocol/packets.rs @@ -321,6 +321,66 @@ macro_rules! def_bitfield { } } +macro_rules! def_packet_group { + ( + $(#[$attrs:meta])* + $group_name:ident { + $($packet:ident),* $(,)? + } + ) => { + #[derive(Clone, Debug)] + $(#[$attrs])* + pub enum $group_name { + $($packet($packet)),* + } + + $( + impl From<$packet> for $group_name { + fn from(p: $packet) -> Self { + Self::$packet(p) + } + } + )* + + impl DecodePacket for $group_name { + fn decode_packet(r: &mut impl Read) -> anyhow::Result { + let packet_id = VarInt::decode(r) + .context(concat!("failed to read ", stringify!($group_name), " packet ID"))?.0; + + match packet_id { + $( + $packet::PACKET_ID => { + let pkt = $packet::decode(r)?; + Ok(Self::$packet(pkt)) + } + )* + id => bail!(concat!("unknown ", stringify!($group_name), " packet ID {:#04x}"), id), + } + } + } + + impl EncodePacket for $group_name { + fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> { + match self { + $( + Self::$packet(pkt) => { + VarInt($packet::PACKET_ID) + .encode(w) + .context(concat!( + "failed to write ", + stringify!($group_name), + " packet ID for ", + stringify!($packet_name) + ))?; + pkt.encode(w) + } + )* + } + } + } + } +} + def_struct! { #[derive(PartialEq, Serialize, Deserialize)] Property { @@ -366,7 +426,7 @@ pub mod status { use super::super::*; def_struct! { - Response 0x00 { + StatusResponse 0x00 { json_response: String } } @@ -428,6 +488,24 @@ pub mod login { threshold: VarInt } } + + def_struct! { + LoginPluginRequest 0x04 { + message_id: VarInt, + channel: Ident, + data: RawBytes, + } + } + + def_packet_group! { + S2cLoginPacket { + Disconnect, + EncryptionRequest, + LoginSuccess, + SetCompression, + LoginPluginRequest, + } + } } pub mod c2s { @@ -460,6 +538,21 @@ pub mod login { sig: Vec, // TODO: bounds? } } + + def_struct! { + LoginPluginResponse 0x02 { + message_id: VarInt, + data: Option, + } + } + + def_packet_group! { + C2sLoginPacket { + LoginStart, + EncryptionResponse, + LoginPluginResponse, + } + } } } @@ -1081,99 +1174,46 @@ pub mod play { } } - macro_rules! def_s2c_play_packet_enum { - { - $($packet:ident),* $(,)? - } => { - /// An enum of all s2c play packets. - #[derive(Clone, Debug)] - pub enum S2cPlayPacket { - $($packet($packet)),* - } - - $( - impl From<$packet> for S2cPlayPacket { - fn from(p: $packet) -> S2cPlayPacket { - S2cPlayPacket::$packet(p) - } - } - )* - - impl EncodePacket for S2cPlayPacket { - fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> { - match self { - $( - Self::$packet(p) => { - VarInt($packet::PACKET_ID) - .encode(w) - .context(concat!("failed to write s2c play packet ID for `", stringify!($packet), "`"))?; - p.encode(w) - } - )* - } - } - } - - #[cfg(test)] - #[test] - fn s2c_play_packet_order() { - let ids = [ - $( - (stringify!($packet), $packet::PACKET_ID), - )* - ]; - - if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) { - panic!( - "the {} (ID {:#x}) and {} (ID {:#x}) variants of the s2c play packet enum are not properly sorted by their packet ID", - w[0].0, - w[0].1, - w[1].0, - w[1].1 - ); - } - } + def_packet_group! { + S2cPlayPacket { + AddEntity, + AddExperienceOrb, + AddPlayer, + Animate, + BlockChangeAck, + BlockDestruction, + BlockEntityData, + BlockEvent, + BlockUpdate, + BossEvent, + Disconnect, + EntityEvent, + ForgetLevelChunk, + GameEvent, + KeepAlive, + LevelChunkWithLight, + Login, + MoveEntityPosition, + MoveEntityPositionAndRotation, + MoveEntityRotation, + PlayerChat, + PlayerInfo, + PlayerPosition, + RemoveEntities, + RotateHead, + SectionBlocksUpdate, + SetCarriedItem, + SetChunkCacheCenter, + SetChunkCacheRadius, + SpawnPosition, + SetEntityMetadata, + SetEntityMotion, + SetTime, + SystemChat, + TabList, + TeleportEntity, } } - - def_s2c_play_packet_enum! { - AddEntity, - AddExperienceOrb, - AddPlayer, - Animate, - BlockChangeAck, - BlockDestruction, - BlockEntityData, - BlockEvent, - BlockUpdate, - BossEvent, - Disconnect, - EntityEvent, - ForgetLevelChunk, - GameEvent, - KeepAlive, - LevelChunkWithLight, - Login, - MoveEntityPosition, - MoveEntityPositionAndRotation, - MoveEntityRotation, - PlayerChat, - PlayerInfo, - PlayerPosition, - RemoveEntities, - RotateHead, - SectionBlocksUpdate, - SetCarriedItem, - SetChunkCacheCenter, - SetChunkCacheRadius, - SpawnPosition, - SetEntityMetadata, - SetEntityMotion, - SetTime, - SystemChat, - TabList, - TeleportEntity, - } } pub mod c2s { @@ -1737,104 +1777,58 @@ pub mod play { } } - macro_rules! def_c2s_play_packet_enum { - { - $($packet:ident),* $(,)? - } => { - /// An enum of all client-to-server play packets. - #[derive(Clone, Debug)] - pub enum C2sPlayPacket { - $($packet($packet)),* + def_packet_group! { + C2sPlayPacket { + AcceptTeleportation, + BlockEntityTagQuery, + ChangeDifficulty, + ChatCommand, + Chat, + ChatPreview, + ClientCommand, + ClientInformation, + CommandSuggestion, + ContainerButtonClick, + ContainerClose, + CustomPayload, + EditBook, + EntityTagQuery, + Interact, + JigsawGenerate, + KeepAlive, + LockDifficulty, + MovePlayerPosition, + MovePlayerPositionAndRotation, + MovePlayerRotation, + MovePlayerStatusOnly, + MoveVehicle, + PaddleBoat, + PickItem, + PlaceRecipe, + PlayerAbilities, + PlayerAction, + PlayerCommand, + PlayerInput, + Pong, + RecipeBookChangeSettings, + RecipeBookSeenRecipe, + RenameItem, + ResourcePack, + SeenAdvancements, + SelectTrade, + SetBeacon, + SetCarriedItem, + SetCommandBlock, + SetCommandBlockMinecart, + SetCreativeModeSlot, + SetJigsawBlock, + SetStructureBlock, + SignUpdate, + Swing, + TeleportToEntity, + UseItemOn, + UseItem, } - - impl DecodePacket for C2sPlayPacket { - fn decode_packet(r: &mut impl Read) -> anyhow::Result { - let packet_id = VarInt::decode(r).context("failed to read c2s play packet ID")?.0; - match packet_id { - $( - $packet::PACKET_ID => { - let pkt = $packet::decode(r)?; - Ok(C2sPlayPacket::$packet(pkt)) - } - )* - id => bail!("unknown c2s play packet ID {:#04x}", id) - } - } - } - - - #[cfg(test)] - #[test] - fn c2s_play_packet_order() { - let ids = [ - $( - (stringify!($packet), $packet::PACKET_ID), - )* - ]; - - if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) { - panic!( - "the {} (ID {:#x}) and {} (ID {:#x}) variants of the c2s play packet enum are not properly sorted by their packet ID", - w[0].0, - w[0].1, - w[1].0, - w[1].1 - ); - } - } - } - } - - def_c2s_play_packet_enum! { - AcceptTeleportation, - BlockEntityTagQuery, - ChangeDifficulty, - ChatCommand, - Chat, - ChatPreview, - ClientCommand, - ClientInformation, - CommandSuggestion, - ContainerButtonClick, - ContainerClose, - CustomPayload, - EditBook, - EntityTagQuery, - Interact, - JigsawGenerate, - KeepAlive, - LockDifficulty, - MovePlayerPosition, - MovePlayerPositionAndRotation, - MovePlayerRotation, - MovePlayerStatusOnly, - MoveVehicle, - PaddleBoat, - PickItem, - PlaceRecipe, - PlayerAbilities, - PlayerAction, - PlayerCommand, - PlayerInput, - Pong, - RecipeBookChangeSettings, - RecipeBookSeenRecipe, - RenameItem, - ResourcePack, - SeenAdvancements, - SelectTrade, - SetBeacon, - SetCarriedItem, - SetCommandBlock, - SetCommandBlockMinecart, - SetCreativeModeSlot, - SetJigsawBlock, - SetStructureBlock, - SignUpdate, - Swing, - TeleportToEntity, - UseItemOn, - UseItem, } } } diff --git a/src/server.rs b/src/server.rs index 51a8b58..239cdef 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,7 +34,7 @@ use crate::protocol::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetC use crate::protocol::packets::play::c2s::C2sPlayPacket; use crate::protocol::packets::play::s2c::S2cPlayPacket; use crate::protocol::packets::status::c2s::{PingRequest, StatusRequest}; -use crate::protocol::packets::status::s2c::{PongResponse, Response}; +use crate::protocol::packets::status::s2c::{PongResponse, StatusResponse}; use crate::protocol::packets::{login, Property}; use crate::protocol::{BoundedArray, BoundedString, VarInt}; use crate::util::valid_username; @@ -547,7 +547,7 @@ async fn handle_status( .insert("favicon".to_string(), Value::String(buf)); } - c.0.write_packet(&Response { + c.0.write_packet(&StatusResponse { json_response: json.to_string(), }) .await?; diff --git a/src/text.rs b/src/text.rs index f82c897..786b33a 100644 --- a/src/text.rs +++ b/src/text.rs @@ -275,8 +275,13 @@ pub trait TextFormat: Into { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(untagged)] enum TextContent { - Text { text: Cow<'static, str> }, - // TODO: translate + Text { + text: Cow<'static, str>, + }, + Translate { + translate: Cow<'static, str>, + // TODO: 'with' field + }, // TODO: score // TODO: entity names // TODO: keybind @@ -320,15 +325,24 @@ enum HoverEvent { }, } +#[allow(clippy::self_named_constructors)] impl Text { pub fn text(plain: impl Into>) -> Self { - #![allow(clippy::self_named_constructors)] Self { content: TextContent::Text { text: plain.into() }, ..Self::default() } } + pub fn translate(key: impl Into>) -> Self { + Self { + content: TextContent::Translate { + translate: key.into(), + }, + ..Self::default() + } + } + pub fn to_plain(&self) -> String { let mut res = String::new(); self.write_plain(&mut res)