forge: add FML2 protocol support (1.13.2-1.16.5+), fixes #400 (#494)

Allows connecting to newer Forge servers, 1.13.2 to 1.16.5 at least, which use
the FML2 handshake protocol:

https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29

Tested with a modded 1.16.5 server

Extends #88 #134 Forge FML (v1, for 1.7.10 - 1.12.2)

* protocol: update Cargo.lock
* protocol: send FML2 on fmlNetworkVersion: 2
* protocol: factor out read_raw_packet_from()
* protocol: move plugin message writing from Server to Conn; add write_fml2_handshake_plugin_message
* protocol: CommandNode: add forge:modid, forge:enum
* forge: add fml2 handshake packets
* server: handle fml:loginwrapper fml::handshake packets
This commit is contained in:
iceiix 2021-01-23 13:47:30 -08:00 committed by GitHub
parent 1a257e2e90
commit 6b961622aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 321 additions and 45 deletions

View File

@ -55,7 +55,7 @@ development is not in lock-step with the server version. The level of
support varies, but the goal is to support major versions from 1.7.10 support varies, but the goal is to support major versions from 1.7.10
up to the current latest major version. Occasionally, snapshots are also supported. up to the current latest major version. Occasionally, snapshots are also supported.
Forge servers are currently supported on 1.7.10 - 1.12.2. Forge servers are supported on 1.7.10 - 1.12.2 (FML) and 1.13.2 - 1.16.5 (FML2).
Support for older protocols will _not_ be dropped as newer protocols are added. Support for older protocols will _not_ be dropped as newer protocols are added.

4
protocol/Cargo.lock generated
View File

@ -840,9 +840,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.119" version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3" checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab"
[[package]] [[package]]
name = "serde_json" name = "serde_json"

View File

@ -191,3 +191,121 @@ impl Serializable for FmlHs {
} }
} }
} }
pub mod fml2 {
// https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29
use super::*;
#[derive(Clone, Default, Debug)]
pub struct Channel {
pub name: String,
pub version: String,
}
impl Serializable for Channel {
fn read_from<R: io::Read>(buf: &mut R) -> Result<Self, Error> {
Ok(Channel {
name: Serializable::read_from(buf)?,
version: Serializable::read_from(buf)?,
})
}
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
self.name.write_to(buf)?;
self.version.write_to(buf)
}
}
#[derive(Clone, Default, Debug)]
pub struct Registry {
pub name: String,
pub marker: String,
}
impl Serializable for Registry {
fn read_from<R: io::Read>(buf: &mut R) -> Result<Self, Error> {
Ok(Registry {
name: Serializable::read_from(buf)?,
marker: "".to_string(), // not in ModList
})
}
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
self.name.write_to(buf)?;
self.marker.write_to(buf)
}
}
#[derive(Debug)]
pub enum FmlHandshake {
ModList {
mod_names: LenPrefixed<VarInt, String>,
channels: LenPrefixed<VarInt, Channel>,
registries: LenPrefixed<VarInt, Registry>,
},
ModListReply {
mod_names: LenPrefixed<VarInt, String>,
channels: LenPrefixed<VarInt, Channel>,
registries: LenPrefixed<VarInt, Registry>,
},
ServerRegistry {
name: String,
snapshot_present: bool,
snapshot: Vec<u8>,
},
ConfigurationData {
filename: String,
contents: Vec<u8>,
},
Acknowledgement,
}
impl FmlHandshake {
pub fn packet_by_id<R: io::Read>(id: i32, buf: &mut R) -> Result<Self, Error> {
Ok(match id {
1 => FmlHandshake::ModList {
mod_names: Serializable::read_from(buf)?,
channels: Serializable::read_from(buf)?,
registries: Serializable::read_from(buf)?,
},
3 => FmlHandshake::ServerRegistry {
name: Serializable::read_from(buf)?,
snapshot_present: Serializable::read_from(buf)?,
snapshot: Serializable::read_from(buf)?,
},
4 => FmlHandshake::ConfigurationData {
filename: Serializable::read_from(buf)?,
contents: Serializable::read_from(buf)?,
},
_ => unimplemented!(),
})
}
}
impl Serializable for FmlHandshake {
fn read_from<R: io::Read>(_buf: &mut R) -> Result<Self, Error> {
unimplemented!()
}
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
match self {
FmlHandshake::ModListReply {
mod_names,
channels,
registries,
} => {
VarInt(2).write_to(buf)?;
mod_names.write_to(buf)?;
channels.write_to(buf)?;
registries.write_to(buf)
}
FmlHandshake::Acknowledgement => VarInt(99).write_to(buf),
_ => unimplemented!(),
}
}
}
}

View File

@ -971,7 +971,7 @@ pub enum Direction {
/// The protocol has multiple 'sub-protocols' or states which control which /// The protocol has multiple 'sub-protocols' or states which control which
/// packet an id points to. /// packet an id points to.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum State { pub enum State {
Handshaking, Handshaking,
Play, Play,
@ -1036,7 +1036,7 @@ pub struct Conn {
cipher: Option<Aes128Cfb>, cipher: Option<Aes128Cfb>,
compression_threshold: i32, pub compression_threshold: i32,
} }
impl Conn { impl Conn {
@ -1099,17 +1099,91 @@ impl Conn {
} }
self.write_all(&buf)?; self.write_all(&buf)?;
Result::Ok(()) Ok(())
} }
pub fn read_packet(&mut self) -> Result<packet::Packet, Error> { pub fn write_plugin_message(&mut self, channel: &str, data: &[u8]) -> Result<(), Error> {
let len = VarInt::read_from(self)?.0 as usize; if is_network_debug() {
debug!(
"Sending plugin message: channel={}, data={:?}",
channel, data
);
}
debug_assert!(self.state == State::Play);
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: LenPrefixedBytes::<VarShort>::new(data.to_vec()),
})?;
}
Ok(())
}
pub fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) -> Result<(), Error> {
let mut buf: Vec<u8> = vec![];
msg.write_to(&mut buf)?;
self.write_plugin_message("FML|HS", &buf)
}
pub fn write_login_plugin_response(
&mut self,
message_id: VarInt,
successful: bool,
data: &[u8],
) -> Result<(), Error> {
if is_network_debug() {
debug!(
"Sending login plugin message: message_id={:?}, successful={:?}, data={:?}",
message_id, successful, data,
);
}
debug_assert!(self.state == State::Login);
self.write_packet(packet::login::serverbound::LoginPluginResponse {
message_id,
successful,
data: data.to_vec(),
})
}
pub fn write_fml2_handshake_plugin_message(
&mut self,
message_id: VarInt,
msg: Option<&forge::fml2::FmlHandshake>,
) -> Result<(), Error> {
if let Some(msg) = msg {
let mut inner_buf: Vec<u8> = vec![];
msg.write_to(&mut inner_buf)?;
let mut outer_buf: Vec<u8> = vec![];
"fml:handshake".to_string().write_to(&mut outer_buf)?;
VarInt(inner_buf.len() as i32).write_to(&mut outer_buf)?;
inner_buf.write_to(&mut outer_buf)?;
self.write_login_plugin_response(message_id, true, &outer_buf)
} else {
unimplemented!() // successful: false, no payload
}
}
#[allow(clippy::type_complexity)]
pub fn read_raw_packet_from<R: io::Read>(
buf: &mut R,
compression_threshold: i32,
) -> Result<(i32, Box<io::Cursor<Vec<u8>>>), Error> {
let len = VarInt::read_from(buf)?.0 as usize;
let mut ibuf = vec![0; len]; let mut ibuf = vec![0; len];
self.read_exact(&mut ibuf)?; buf.read_exact(&mut ibuf)?;
let mut buf = io::Cursor::new(ibuf); let mut buf = io::Cursor::new(ibuf);
if self.compression_threshold >= 0 { if compression_threshold >= 0 {
let uncompressed_size = VarInt::read_from(&mut buf)?.0; let uncompressed_size = VarInt::read_from(&mut buf)?.0;
if uncompressed_size != 0 { if uncompressed_size != 0 {
let mut new = Vec::with_capacity(uncompressed_size as usize); let mut new = Vec::with_capacity(uncompressed_size as usize);
@ -1120,7 +1194,7 @@ impl Conn {
if is_network_debug() { if is_network_debug() {
debug!( debug!(
"Decompressed threshold={} len={} uncompressed_size={} to {} bytes", "Decompressed threshold={} len={} uncompressed_size={} to {} bytes",
self.compression_threshold, compression_threshold,
len, len,
uncompressed_size, uncompressed_size,
new.len() new.len()
@ -1131,6 +1205,13 @@ impl Conn {
} }
let id = VarInt::read_from(&mut buf)?.0; let id = VarInt::read_from(&mut buf)?.0;
Ok((id, Box::new(buf)))
}
pub fn read_packet(&mut self) -> Result<packet::Packet, Error> {
let compression_threshold = self.compression_threshold;
let (id, mut buf) = Conn::read_raw_packet_from(self, compression_threshold)?;
let dir = match self.direction { let dir = match self.direction {
Direction::Clientbound => Direction::Serverbound, Direction::Clientbound => Direction::Serverbound,
Direction::Serverbound => Direction::Clientbound, Direction::Serverbound => Direction::Clientbound,
@ -1224,6 +1305,7 @@ impl Conn {
// For modded servers, get the list of Forge mods installed // For modded servers, get the list of Forge mods installed
let mut forge_mods: std::vec::Vec<crate::protocol::forge::ForgeMod> = vec![]; let mut forge_mods: std::vec::Vec<crate::protocol::forge::ForgeMod> = vec![];
let mut fml_network_version: Option<i64> = None;
if let Some(modinfo) = val.get("modinfo") { if let Some(modinfo) = val.get("modinfo") {
if let Some(modinfo_type) = modinfo.get("type") { if let Some(modinfo_type) = modinfo.get("type") {
if modinfo_type == "FML" { if modinfo_type == "FML" {
@ -1240,6 +1322,7 @@ impl Conn {
.push(crate::protocol::forge::ForgeMod { modid, version }); .push(crate::protocol::forge::ForgeMod { modid, version });
} }
} }
fml_network_version = Some(1);
} }
} }
} else { } else {
@ -1267,6 +1350,13 @@ impl Conn {
} }
} }
} }
fml_network_version = Some(
forge_data
.get("fmlNetworkVersion")
.unwrap()
.as_i64()
.unwrap(),
);
} }
Ok(( Ok((
@ -1301,6 +1391,7 @@ impl Conn {
.and_then(Value::as_str) .and_then(Value::as_str)
.map(|v| v.to_owned()), .map(|v| v.to_owned()),
forge_mods, forge_mods,
fml_network_version,
}, },
ping, ping,
)) ))
@ -1352,6 +1443,7 @@ pub struct Status {
pub description: format::Component, pub description: format::Component,
pub favicon: Option<String>, pub favicon: Option<String>,
pub forge_mods: Vec<crate::protocol::forge::ForgeMod>, pub forge_mods: Vec<crate::protocol::forge::ForgeMod>,
pub fml_network_version: Option<i64>,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -3135,6 +3135,10 @@ pub enum CommandProperty {
EntitySummon, EntitySummon,
Dimension, Dimension,
UUID, UUID,
ForgeModId,
ForgeEnum {
cls: String,
},
} }
impl Serializable for CommandNode { impl Serializable for CommandNode {
@ -3264,6 +3268,10 @@ impl Serializable for CommandNode {
"minecraft:entity_summon" => CommandProperty::EntitySummon, "minecraft:entity_summon" => CommandProperty::EntitySummon,
"minecraft:dimension" => CommandProperty::Dimension, "minecraft:dimension" => CommandProperty::Dimension,
"minecraft:uuid" => CommandProperty::UUID, "minecraft:uuid" => CommandProperty::UUID,
"forge:modid" => CommandProperty::ForgeModId,
"forge:enum" => CommandProperty::ForgeEnum {
cls: Serializable::read_from(buf)?,
},
_ => panic!("unsupported command node parser {}", parse), _ => panic!("unsupported command node parser {}", parse),
}) })
} else { } else {

View File

@ -91,7 +91,7 @@ pub struct Game {
impl Game { impl Game {
pub fn connect_to(&mut self, address: &str) { pub fn connect_to(&mut self, address: &str) {
let (protocol_version, forge_mods) = let (protocol_version, forge_mods, fml_network_version) =
match protocol::Conn::new(&address, self.default_protocol_version) match protocol::Conn::new(&address, self.default_protocol_version)
.and_then(|conn| conn.do_status()) .and_then(|conn| conn.do_status())
{ {
@ -100,14 +100,18 @@ impl Game {
"Detected server protocol version {}", "Detected server protocol version {}",
res.0.version.protocol res.0.version.protocol
); );
(res.0.version.protocol, res.0.forge_mods) (
res.0.version.protocol,
res.0.forge_mods,
res.0.fml_network_version,
)
} }
Err(err) => { Err(err) => {
warn!( warn!(
"Error pinging server {} to get protocol version: {:?}, defaulting to {}", "Error pinging server {} to get protocol version: {:?}, defaulting to {}",
address, err, self.default_protocol_version address, err, self.default_protocol_version
); );
(self.default_protocol_version, vec![]) (self.default_protocol_version, vec![], None)
} }
}; };
@ -127,6 +131,7 @@ impl Game {
&address, &address,
protocol_version, protocol_version,
forge_mods, forge_mods,
fml_network_version,
)) ))
.unwrap(); .unwrap();
}); });

View File

@ -25,7 +25,7 @@ use crate::types::Gamemode;
use crate::world; use crate::world;
use crate::world::block; use crate::world::block;
use cgmath::prelude::*; use cgmath::prelude::*;
use log::{debug, error, warn}; use log::{debug, error, info, warn};
use rand::{self, Rng}; use rand::{self, Rng};
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::BuildHasherDefault; use std::hash::BuildHasherDefault;
@ -110,14 +110,17 @@ impl Server {
address: &str, address: &str,
protocol_version: i32, protocol_version: i32,
forge_mods: Vec<forge::ForgeMod>, forge_mods: Vec<forge::ForgeMod>,
fml_network_version: Option<i64>,
) -> Result<Server, protocol::Error> { ) -> Result<Server, protocol::Error> {
let mut conn = protocol::Conn::new(address, protocol_version)?; let mut conn = protocol::Conn::new(address, protocol_version)?;
let tag = if !forge_mods.is_empty() { let tag = match fml_network_version {
"\0FML\0" Some(1) => "\0FML\0",
} else { Some(2) => "\0FML2\0",
"" None => "",
_ => panic!("unsupported FML network version: {:?}", fml_network_version),
}; };
let host = conn.host.clone() + tag; let host = conn.host.clone() + tag;
let port = conn.port; let port = conn.port;
conn.write_packet(protocol::packet::handshake::serverbound::Handshake { conn.write_packet(protocol::packet::handshake::serverbound::Handshake {
@ -167,7 +170,6 @@ impl Server {
Some(rx), Some(rx),
)); ));
} }
// TODO: avoid duplication
protocol::packet::Packet::LoginSuccess_UUID(val) => { protocol::packet::Packet::LoginSuccess_UUID(val) => {
warn!("Server is running in offline mode"); warn!("Server is running in offline mode");
debug!("Login: {} {:?}", val.username, val.uuid); debug!("Login: {} {:?}", val.username, val.uuid);
@ -188,7 +190,7 @@ impl Server {
protocol::packet::Packet::LoginDisconnect(val) => { protocol::packet::Packet::LoginDisconnect(val) => {
return Err(protocol::Error::Disconnect(val.reason)) return Err(protocol::Error::Disconnect(val.reason))
} }
val => return Err(protocol::Error::Err(format!("Wrong packet: {:?}", val))), val => return Err(protocol::Error::Err(format!("Wrong packet 1: {:?}", val))),
}; };
} }
@ -225,6 +227,7 @@ impl Server {
write.enable_encyption(&shared, false); write.enable_encyption(&shared, false);
let uuid; let uuid;
let compression_threshold = read.compression_threshold;
loop { loop {
match read.read_packet()? { match read.read_packet()? {
protocol::packet::Packet::SetInitialCompression(val) => { protocol::packet::Packet::SetInitialCompression(val) => {
@ -248,7 +251,73 @@ impl Server {
protocol::packet::Packet::LoginDisconnect(val) => { protocol::packet::Packet::LoginDisconnect(val) => {
return Err(protocol::Error::Disconnect(val.reason)) return Err(protocol::Error::Disconnect(val.reason))
} }
val => return Err(protocol::Error::Err(format!("Wrong packet: {:?}", val))), protocol::packet::Packet::LoginPluginRequest(req) => {
match req.channel.as_ref() {
"fml:loginwrapper" => {
let mut cursor = std::io::Cursor::new(req.data);
let channel: String = protocol::Serializable::read_from(&mut cursor)?;
let (id, mut data) = protocol::Conn::read_raw_packet_from(
&mut cursor,
compression_threshold,
)?;
match channel.as_ref() {
"fml:handshake" => {
let packet =
forge::fml2::FmlHandshake::packet_by_id(id, &mut data)?;
use forge::fml2::FmlHandshake::*;
match packet {
ModList {
mod_names,
channels,
registries,
} => {
info!("ModList mod_names={:?} channels={:?} registries={:?}", mod_names, channels, registries);
write.write_fml2_handshake_plugin_message(
req.message_id,
Some(&ModListReply {
mod_names,
channels,
registries,
}),
)?;
}
ServerRegistry {
name,
snapshot_present: _,
snapshot: _,
} => {
info!("ServerRegistry {:?}", name);
write.write_fml2_handshake_plugin_message(
req.message_id,
Some(&Acknowledgement),
)?;
}
ConfigurationData { filename, contents } => {
info!(
"ConfigurationData filename={:?} contents={}",
filename,
String::from_utf8_lossy(&contents)
);
write.write_fml2_handshake_plugin_message(
req.message_id,
Some(&Acknowledgement),
)?;
}
_ => unimplemented!(),
}
}
_ => panic!(
"unknown LoginPluginRequest fml:loginwrapper channel: {:?}",
channel
),
}
}
_ => panic!("unsupported LoginPluginRequest channel: {:?}", req.channel),
}
}
val => return Err(protocol::Error::Err(format!("Wrong packet 2: {:?}", val))),
} }
} }
@ -946,33 +1015,17 @@ impl Server {
} }
} }
// TODO: remove wrappers and directly call on Conn
fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) { fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) {
use crate::protocol::Serializable; let _ = self.conn.as_mut().unwrap().write_fmlhs_plugin_message(msg); // TODO handle errors
let mut buf: Vec<u8> = vec![];
msg.write_to(&mut buf).unwrap();
self.write_plugin_message("FML|HS", &buf);
} }
fn write_plugin_message(&mut self, channel: &str, data: &[u8]) { fn write_plugin_message(&mut self, channel: &str, data: &[u8]) {
if protocol::is_network_debug() { let _ = self
debug!( .conn
"Sending plugin message: channel={}, data={:?}", .as_mut()
channel, data .unwrap()
); .write_plugin_message(channel, data); // TODO handle errors
}
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::<protocol::VarShort>::new(data.to_vec()),
});
}
} }
fn on_game_join_worldnames_ishard( fn on_game_join_worldnames_ishard(