From 327efcf0435ddbae4ffcd7b875ebdf8d013346a7 Mon Sep 17 00:00:00 2001 From: iceiix <43691553+iceiix@users.noreply.github.com> Date: Sun, 5 May 2019 18:37:37 -0700 Subject: [PATCH] Add Forge handshake support. Closes #88 (#134) Adds support for connecting to 1.7.10 modded servers using the FML|HS protocol: https://wiki.vg/Minecraft_Forge_Handshake * Handle client-bound plugin message packets * Parse FML|HS plugin channel messages * Add ModList serialization using Mod serializable, LenPrefixed * Save forge_mods from server ping and send in FML|HS ModList packet * Show Forge mod count in server ping listing * Send acknowledgements, completing the handshake * Add VarShort to custom payload len prefix replaces i16, fixes OOM on large modded servers * Add custom CoFHLib's SendUUID packet -26 See explanation at https://github.com/SpigotMC/BungeeCord/issues/1437 This packet is defined by CoFHLib in https://github.com/CoFH/CoFHLib/blob/1.7.10/src/main/java/cofh/lib/util/helpers/SecurityHelper.java#L40 Fixes thread '' panicked at 'bad packet id 0xffffffe6 in Clientbound Play' with FTB:IE --- README.md | 2 +- src/main.rs | 23 ++-- src/protocol/forge.rs | 180 +++++++++++++++++++++++++++++++ src/protocol/mod.rs | 85 +++++++++++++++ src/protocol/packet.rs | 7 +- src/protocol/versions/v1_7_10.rs | 1 + src/screen/server_list.rs | 7 +- src/server/mod.rs | 96 ++++++++++++++++- src/server/plugin_messages.rs | 4 +- 9 files changed, 383 insertions(+), 22 deletions(-) create mode 100644 src/protocol/forge.rs diff --git a/README.md b/README.md index ad4e905..de56e7b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Join with your favorite IRC client or [Matrix](https://matrix.to/#/#_espernet_#s | 1.9 | 107 | ✓ | | 15w39c | 74 | ✓ | | 1.8.9 | 47 | ✓ | -| 1.7.10 | 5 | ✓ | +| 1.7.10 + Forge | 5 | ✓ | Stevenarella is designed to support multiple protocol versions, so that client development is not in lock-step with the server version. The level of diff --git a/src/main.rs b/src/main.rs index e22fe97..a469931 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,16 +88,17 @@ pub struct Game { impl Game { pub fn connect_to(&mut self, address: &str) { - let protocol_version = match protocol::Conn::new(&address, protocol::SUPPORTED_PROTOCOLS[0]).and_then(|conn| conn.do_status()) { - Ok(res) => { - info!("Detected server protocol version {}", res.0.version.protocol); - res.0.version.protocol - }, - Err(err) => { - warn!("Error pinging server {} to get protocol version: {:?}, defaulting to {}", address, err, protocol::SUPPORTED_PROTOCOLS[0]); - protocol::SUPPORTED_PROTOCOLS[0] - }, - }; + let (protocol_version, forge_mods) = match protocol::Conn::new(&address, protocol::SUPPORTED_PROTOCOLS[0]) + .and_then(|conn| conn.do_status()) { + Ok(res) => { + info!("Detected server protocol version {}", res.0.version.protocol); + (res.0.version.protocol, res.0.forge_mods) + }, + Err(err) => { + warn!("Error pinging server {} to get protocol version: {:?}, defaulting to {}", address, err, protocol::SUPPORTED_PROTOCOLS[0]); + (protocol::SUPPORTED_PROTOCOLS[0], vec![]) + }, + }; let (tx, rx) = mpsc::channel(); self.connect_reply = Some(rx); @@ -109,7 +110,7 @@ impl Game { access_token: self.vars.get(auth::AUTH_TOKEN).clone(), }; thread::spawn(move || { - tx.send(server::Server::connect(resources, profile, &address, protocol_version)).unwrap(); + tx.send(server::Server::connect(resources, profile, &address, protocol_version, forge_mods)).unwrap(); }); } diff --git a/src/protocol/forge.rs b/src/protocol/forge.rs new file mode 100644 index 0000000..21d47b3 --- /dev/null +++ b/src/protocol/forge.rs @@ -0,0 +1,180 @@ + +/// Implements https://wiki.vg/Minecraft_Forge_Handshake +use std::io; +use byteorder::WriteBytesExt; + +use crate::protocol::{Serializable, Error, LenPrefixed, VarInt}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Phase { + // Client handshake states (written) + Start, + WaitingServerData, + WaitingServerComplete, + PendingComplete, + + // Server handshake states (read) + WaitingCAck, + + // Both client and server handshake states (different values on the wire) + Complete, +} + +impl Serializable for Phase { + /// Read server handshake state from server + fn read_from(buf: &mut R) -> Result { + let phase: i8 = Serializable::read_from(buf)?; + Ok(match phase { + 2 => Phase::WaitingCAck, + 3 => Phase::Complete, + _ => panic!("bad FML|HS server phase: {}", phase), + }) + } + + /// Send client handshake state from client + fn write_to(&self, buf: &mut W) -> Result<(), Error> { + buf.write_u8(match self { + Phase::WaitingServerData => 2, + Phase::WaitingServerComplete => 3, + Phase::PendingComplete => 4, + Phase::Complete => 5, + _ => panic!("bad FML|HS client phase: {:?}", self), + })?; + Ok(()) + } +} + + +#[derive(Clone, Debug, Default)] +pub struct ForgeMod { + pub modid: String, + pub version: String, +} + +impl Serializable for ForgeMod { + fn read_from(buf: &mut R) -> Result { + Ok(ForgeMod { + modid: Serializable::read_from(buf)?, + version: Serializable::read_from(buf)?, + }) + } + + fn write_to(&self, buf: &mut W) -> Result<(), Error> { + self.modid.write_to(buf)?; + self.version.write_to(buf) + } +} + +#[derive(Debug)] +pub struct ModIdMapping { + pub name: String, + pub id: VarInt, +} + +impl Serializable for ModIdMapping { + fn read_from(buf: &mut R) -> Result { + Ok(ModIdMapping { + name: Serializable::read_from(buf)?, + id: Serializable::read_from(buf)?, + }) + } + + fn write_to(&self, buf: &mut W) -> Result<(), Error> { + self.name.write_to(buf)?; + self.id.write_to(buf) + } +} + +#[derive(Debug)] +pub enum FmlHs { + ServerHello { + fml_protocol_version: i8, + override_dimension: Option, + }, + ClientHello { + fml_protocol_version: i8, + }, + ModList { + mods: LenPrefixed, + }, + /* TODO: 1.8+ https://wiki.vg/Minecraft_Forge_Handshake#Differences_from_Forge_1.7.10 + RegistryData { + has_more: bool, + name: String, + ids: LenPrefixed, + substitutions: LenPrefixed, + dummies: LenPrefixed, + }, + */ + ModIdData { + mappings: LenPrefixed, + block_substitutions: LenPrefixed, + item_substitutions: LenPrefixed, + }, + HandshakeAck { + phase: Phase, + }, + HandshakeReset, +} + +impl Serializable for FmlHs { + fn read_from(buf: &mut R) -> Result { + let discriminator: u8 = Serializable::read_from(buf)?; + + match discriminator { + 0 => { + let fml_protocol_version: i8 = Serializable::read_from(buf)?; + let override_dimension = if fml_protocol_version > 1 { + let dimension: i32 = Serializable::read_from(buf)?; + Some(dimension) + } else { + None + }; + + println!("FML|HS ServerHello: fml_protocol_version={}, override_dimension={:?}", fml_protocol_version, override_dimension); + + Ok(FmlHs::ServerHello { + fml_protocol_version, + override_dimension, + }) + }, + 1 => panic!("Received unexpected FML|HS ClientHello from server"), + 2 => { + Ok(FmlHs::ModList { + mods: Serializable::read_from(buf)?, + }) + }, + 3 => { + Ok(FmlHs::ModIdData { + mappings: Serializable::read_from(buf)?, + block_substitutions: Serializable::read_from(buf)?, + item_substitutions: Serializable::read_from(buf)?, + }) + }, + 255 => { + Ok(FmlHs::HandshakeAck { + phase: Serializable::read_from(buf)?, + }) + }, + _ => panic!("Unhandled FML|HS packet: discriminator={}", discriminator), + } + } + + fn write_to(&self, buf: &mut W) -> Result<(), Error> { + match self { + FmlHs::ClientHello { fml_protocol_version } => { + buf.write_u8(1)?; + fml_protocol_version.write_to(buf) + }, + FmlHs::ModList { mods } => { + buf.write_u8(2)?; + mods.write_to(buf) + }, + FmlHs::HandshakeAck { phase } => { + buf.write_u8(255)?; + phase.write_to(buf) + }, + _ => unimplemented!() + } + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index be57ae3..97423ca 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -23,6 +23,7 @@ use serde_json; use reqwest; pub mod mojang; +pub mod forge; use crate::nbt; use crate::format; @@ -660,6 +661,65 @@ impl fmt::Debug for VarInt { } } +/// `VarShort` have a variable size (2 or 3 bytes) and are backwards-compatible +/// with vanilla shorts, used for Forge custom payloads +#[derive(Clone, Copy)] +pub struct VarShort(pub i32); + +impl Lengthable for VarShort { + fn into(self) -> usize { + self.0 as usize + } + + fn from(u: usize) -> VarShort { + VarShort(u as i32) + } +} + +impl Serializable for VarShort { + fn read_from(buf: &mut R) -> Result { + let low = buf.read_u16::()? as u32; + let val = if (low & 0x8000) != 0 { + let high = buf.read_u8()? as u32; + + (high << 15) | (low & 0x7fff) + } else { + low + }; + + Result::Ok(VarShort(val as i32)) + } + + fn write_to(&self, buf: &mut W) -> Result<(), Error> { + assert!(self.0 >= 0 && self.0 <= 0x7fffff, "VarShort invalid value: {}", self.0); + let mut low = self.0 & 0x7fff; + let high = (self.0 & 0x7f8000) >> 15; + if high != 0 { + low |= 0x8000; + } + + buf.write_u16::(low as u16)?; + + if high != 0 { + buf.write_u8(high as u8)?; + } + + Ok(()) + } +} + +impl default::Default for VarShort { + fn default() -> VarShort { + VarShort(0) + } +} + +impl fmt::Debug for VarShort { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + /// `VarLong` have a variable size (between 1 and 10 bytes) when encoded based /// on the size of the number #[derive(Clone, Copy)] @@ -1005,6 +1065,29 @@ impl Conn { let version = val.get("version").ok_or(invalid_status())?; let players = val.get("players").ok_or(invalid_status())?; + // For modded servers, get the list of Forge mods installed + let mut forge_mods: std::vec::Vec = vec![]; + if let Some(modinfo) = val.get("modinfo") { + if let Some(modinfo_type) = modinfo.get("type") { + if modinfo_type == "FML" { + if let Some(modlist) = modinfo.get("modList") { + if let Value::Array(items) = modlist { + for item in items { + if let Value::Object(obj) = item { + let modid = obj.get("modid").unwrap().as_str().unwrap().to_string(); + let version = obj.get("version").unwrap().as_str().unwrap().to_string(); + + forge_mods.push(crate::protocol::forge::ForgeMod { modid, version }); + } + } + } + } + } else { + panic!("Unrecognized modinfo type in server ping response: {} in {}", modinfo_type, modinfo); + } + } + } + Ok((Status { version: StatusVersion { name: version.get("name").and_then(Value::as_str).ok_or(invalid_status())? @@ -1025,6 +1108,7 @@ impl Conn { description: format::Component::from_value(val.get("description") .ok_or(invalid_status())?), favicon: val.get("favicon").and_then(Value::as_str).map(|v| v.to_owned()), + forge_mods, }, ping)) } @@ -1036,6 +1120,7 @@ pub struct Status { pub players: StatusPlayers, pub description: format::Component, pub favicon: Option, + pub forge_mods: Vec, } #[derive(Debug)] diff --git a/src/protocol/packet.rs b/src/protocol/packet.rs index 6b75acb..6e78020 100644 --- a/src/protocol/packet.rs +++ b/src/protocol/packet.rs @@ -161,7 +161,7 @@ state_packets!( } packet PluginMessageServerbound_i16 { field channel: String =, - field data: LenPrefixedBytes =, + field data: LenPrefixedBytes =, } packet EditBook { field new_book: Option =, @@ -875,7 +875,7 @@ state_packets!( } packet PluginMessageClientbound_i16 { field channel: String =, - field data: LenPrefixedBytes =, + field data: LenPrefixedBytes =, } /// Plays a sound by name on the client packet NamedSoundEffect { @@ -1747,6 +1747,9 @@ state_packets!( field id: VarInt =, field trades: LenPrefixed =, } + packet CoFHLib_SendUUID { + field player_uuid: UUID =, + } } } login Login { diff --git a/src/protocol/versions/v1_7_10.rs b/src/protocol/versions/v1_7_10.rs index 5ca41d9..1bc92ff 100644 --- a/src/protocol/versions/v1_7_10.rs +++ b/src/protocol/versions/v1_7_10.rs @@ -99,6 +99,7 @@ protocol_packet_ids!( 0x3e => Teams_NoVisColor 0x3f => PluginMessageClientbound_i16 0x40 => Disconnect + -0x1a => CoFHLib_SendUUID } } login Login { diff --git a/src/screen/server_list.rs b/src/screen/server_list.rs index 3c88b89..7381181 100644 --- a/src/screen/server_list.rs +++ b/src/screen/server_list.rs @@ -75,6 +75,7 @@ struct PingInfo { max: i32, protocol_version: i32, protocol_name: String, + forge_mods: Vec, favicon: Option, } @@ -278,6 +279,7 @@ impl ServerList { max: res.0.players.max, protocol_version: res.0.version.protocol, protocol_name: res.0.version.name, + forge_mods: res.0.forge_mods, favicon, })); } @@ -293,6 +295,7 @@ impl ServerList { max: 0, protocol_version: 0, protocol_name: "".to_owned(), + forge_mods: vec![], favicon: None, }); } @@ -489,7 +492,9 @@ impl super::Screen for ServerList { }; players.text = txt; } - let mut txt = TextComponent::new(&res.protocol_name); + let sm = format!("{} mods + {}", res.forge_mods.len(), res.protocol_name); + let st = if res.forge_mods.len() > 0 { &sm } else { &res.protocol_name }; + let mut txt = TextComponent::new(&st); txt.modifier.color = Some(format::Color::Yellow); let mut msg = Component::Text(txt); format::convert_legacy(&mut msg); diff --git a/src/server/mod.rs b/src/server/mod.rs index be1df24..bd5cf6e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::protocol::{self, mojang, packet}; +use crate::protocol::{self, mojang, packet, forge}; use crate::world; use crate::world::block; use rand::{self, Rng}; @@ -42,6 +42,7 @@ pub struct Server { uuid: protocol::UUID, conn: Option, protocol_version: i32, + forge_mods: Vec, read_queue: Option>>, pub disconnect_reason: Option, just_disconnected: bool, @@ -104,7 +105,7 @@ macro_rules! handle_packet { impl Server { - pub fn connect(resources: Arc>, profile: mojang::Profile, address: &str, protocol_version: i32) -> Result { + pub fn connect(resources: Arc>, profile: mojang::Profile, address: &str, protocol_version: i32, forge_mods: Vec) -> Result { let mut conn = protocol::Conn::new(address, protocol_version)?; let host = conn.host.clone(); @@ -147,7 +148,7 @@ impl Server { read.state = protocol::State::Play; write.state = protocol::State::Play; let rx = Self::spawn_reader(read); - return Ok(Server::new(protocol_version, protocol::UUID::from_str(&val.uuid), resources, Some(write), Some(rx))); + return Ok(Server::new(protocol_version, forge_mods, protocol::UUID::from_str(&val.uuid), resources, Some(write), Some(rx))); } protocol::packet::Packet::LoginDisconnect(val) => return Err(protocol::Error::Disconnect(val.reason)), val => return Err(protocol::Error::Err(format!("Wrong packet: {:?}", val))), @@ -205,7 +206,7 @@ impl Server { let rx = Self::spawn_reader(read); - Ok(Server::new(protocol_version, protocol::UUID::from_str(&uuid), resources, Some(write), Some(rx))) + Ok(Server::new(protocol_version, forge_mods, protocol::UUID::from_str(&uuid), resources, Some(write), Some(rx))) } fn spawn_reader(mut read: protocol::Conn) -> mpsc::Receiver> { @@ -226,7 +227,7 @@ impl Server { } pub fn dummy_server(resources: Arc>) -> Server { - let mut server = Server::new(protocol::SUPPORTED_PROTOCOLS[0], protocol::UUID::default(), resources, None, None); + let mut server = Server::new(protocol::SUPPORTED_PROTOCOLS[0], vec![], protocol::UUID::default(), resources, None, None); let mut rng = rand::thread_rng(); for x in -7*16 .. 7*16 { for z in -7*16 .. 7*16 { @@ -263,6 +264,7 @@ impl Server { fn new( protocol_version: i32, + forge_mods: Vec, uuid: protocol::UUID, resources: Arc>, conn: Option, read_queue: Option>> @@ -279,6 +281,7 @@ impl Server { uuid, conn, protocol_version, + forge_mods, read_queue, disconnect_reason: None, just_disconnected: false, @@ -388,6 +391,8 @@ impl Server { match pck { Ok(pck) => handle_packet!{ self pck { + PluginMessageClientbound_i16 => on_plugin_message_clientbound_i16, + PluginMessageClientbound => on_plugin_message_clientbound_1, JoinGame_i32_ViewDistance => on_game_join_i32_viewdistance, JoinGame_i32 => on_game_join_i32, JoinGame_i8 => on_game_join_i8, @@ -667,6 +672,86 @@ impl Server { }); } + fn on_plugin_message_clientbound_i16(&mut self, msg: packet::play::clientbound::PluginMessageClientbound_i16) { + self.on_plugin_message_clientbound(&msg.channel, msg.data.data.as_slice()) + } + + fn on_plugin_message_clientbound_1(&mut self, msg: packet::play::clientbound::PluginMessageClientbound) { + self.on_plugin_message_clientbound(&msg.channel, &msg.data) + } + + fn on_plugin_message_clientbound(&mut self, channel: &str, data: &[u8]) { + println!("Received plugin message: channel={}, data={:?}", channel, data); + + match channel { + // TODO: "REGISTER" => + // TODO: "UNREGISTER" => + "FML|HS" => { + let msg = crate::protocol::Serializable::read_from(&mut std::io::Cursor::new(data)).unwrap(); + println!("FML|HS msg={:?}", msg); + + use forge::FmlHs::*; + use forge::Phase::*; + match msg { + ServerHello { fml_protocol_version, override_dimension } => { + println!("Received FML|HS ServerHello {} {:?}", fml_protocol_version, override_dimension); + + self.write_plugin_message("REGISTER", "FML|HS\0FML\0FML|MP\0FML\0FORGE".as_bytes()); + self.write_fmlhs_plugin_message(&ClientHello { fml_protocol_version }); + // Send stashed mods list received from ping packet, client matching server + let mods = crate::protocol::LenPrefixed::::new(self.forge_mods.clone()); + self.write_fmlhs_plugin_message(&ModList { mods }); + }, + ModList { mods } => { + println!("Received FML|HS ModList: {:?}", mods); + + self.write_fmlhs_plugin_message(&HandshakeAck { phase: WaitingServerData }); + }, + ModIdData { mappings: _, block_substitutions: _, item_substitutions: _ } => { + self.write_fmlhs_plugin_message(&HandshakeAck { phase: WaitingServerData }); + }, + HandshakeAck { phase } => { + match phase { + WaitingCAck => { + self.write_fmlhs_plugin_message(&HandshakeAck { phase: PendingComplete }); + }, + Complete => { + println!("FML|HS handshake complete!"); + }, + _ => unimplemented!(), + } + }, + _ => (), + } + } + _ => () + } + } + + fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) { + use crate::protocol::Serializable; + + let mut buf: Vec = vec![]; + msg.write_to(&mut buf).unwrap(); + + self.write_plugin_message("FML|HS", &buf); + } + + fn write_plugin_message(&mut self, channel: &str, data: &[u8]) { + println!("Sending plugin message: channel={}, data={:?}", channel, data); + if self.protocol_version >= 47 { + self.write_packet(packet::play::serverbound::PluginMessageServerbound { + channel: channel.to_string(), + data: data.to_vec(), + }); + } else { + self.write_packet(packet::play::serverbound::PluginMessageServerbound_i16 { + channel: channel.to_string(), + data: crate::protocol::LenPrefixedBytes::::new(data.to_vec()), + }); + } + } + fn on_game_join_i32_viewdistance(&mut self, join: packet::play::clientbound::JoinGame_i32_ViewDistance) { self.on_game_join(join.gamemode, join.entity_id) } @@ -702,6 +787,7 @@ impl Server { let brand = plugin_messages::Brand { brand: "Steven".into(), }; + // TODO: refactor with write_plugin_message if self.protocol_version >= 47 { self.write_packet(brand.as_message()); } else { diff --git a/src/server/plugin_messages.rs b/src/server/plugin_messages.rs index 3eb9faf..94db273 100644 --- a/src/server/plugin_messages.rs +++ b/src/server/plugin_messages.rs @@ -1,7 +1,7 @@ -use crate::protocol::Serializable; use crate::protocol::packet::play::serverbound::PluginMessageServerbound; use crate::protocol::packet::play::serverbound::PluginMessageServerbound_i16; +use crate::protocol::{Serializable, VarShort}; pub struct Brand { pub brand: String, @@ -31,7 +31,7 @@ impl Brand { Serializable::write_to(&self.brand, &mut data).unwrap(); PluginMessageServerbound_i16 { channel: "MC|Brand".into(), - data: crate::protocol::LenPrefixedBytes::::new(data), + data: crate::protocol::LenPrefixedBytes::::new(data), } }