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
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.

4
protocol/Cargo.lock generated
View File

@ -840,9 +840,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.119"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3"
checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab"
[[package]]
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
/// packet an id points to.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum State {
Handshaking,
Play,
@ -1036,7 +1036,7 @@ pub struct Conn {
cipher: Option<Aes128Cfb>,
compression_threshold: i32,
pub compression_threshold: i32,
}
impl Conn {
@ -1099,17 +1099,91 @@ impl Conn {
}
self.write_all(&buf)?;
Result::Ok(())
Ok(())
}
pub fn read_packet(&mut self) -> Result<packet::Packet, Error> {
let len = VarInt::read_from(self)?.0 as usize;
pub fn write_plugin_message(&mut self, channel: &str, data: &[u8]) -> Result<(), Error> {
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];
self.read_exact(&mut ibuf)?;
buf.read_exact(&mut 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;
if uncompressed_size != 0 {
let mut new = Vec::with_capacity(uncompressed_size as usize);
@ -1120,7 +1194,7 @@ impl Conn {
if is_network_debug() {
debug!(
"Decompressed threshold={} len={} uncompressed_size={} to {} bytes",
self.compression_threshold,
compression_threshold,
len,
uncompressed_size,
new.len()
@ -1131,6 +1205,13 @@ impl Conn {
}
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 {
Direction::Clientbound => Direction::Serverbound,
Direction::Serverbound => Direction::Clientbound,
@ -1224,6 +1305,7 @@ impl Conn {
// For modded servers, get the list of Forge mods installed
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_type) = modinfo.get("type") {
if modinfo_type == "FML" {
@ -1240,6 +1322,7 @@ impl Conn {
.push(crate::protocol::forge::ForgeMod { modid, version });
}
}
fml_network_version = Some(1);
}
}
} else {
@ -1267,6 +1350,13 @@ impl Conn {
}
}
}
fml_network_version = Some(
forge_data
.get("fmlNetworkVersion")
.unwrap()
.as_i64()
.unwrap(),
);
}
Ok((
@ -1301,6 +1391,7 @@ impl Conn {
.and_then(Value::as_str)
.map(|v| v.to_owned()),
forge_mods,
fml_network_version,
},
ping,
))
@ -1352,6 +1443,7 @@ pub struct Status {
pub description: format::Component,
pub favicon: Option<String>,
pub forge_mods: Vec<crate::protocol::forge::ForgeMod>,
pub fml_network_version: Option<i64>,
}
#[derive(Debug)]

View File

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

View File

@ -91,7 +91,7 @@ pub struct Game {
impl Game {
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)
.and_then(|conn| conn.do_status())
{
@ -100,14 +100,18 @@ impl Game {
"Detected server protocol version {}",
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) => {
warn!(
"Error pinging server {} to get protocol version: {:?}, defaulting to {}",
address, err, self.default_protocol_version
);
(self.default_protocol_version, vec![])
(self.default_protocol_version, vec![], None)
}
};
@ -127,6 +131,7 @@ impl Game {
&address,
protocol_version,
forge_mods,
fml_network_version,
))
.unwrap();
});

View File

@ -25,7 +25,7 @@ use crate::types::Gamemode;
use crate::world;
use crate::world::block;
use cgmath::prelude::*;
use log::{debug, error, warn};
use log::{debug, error, info, warn};
use rand::{self, Rng};
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
@ -110,14 +110,17 @@ impl Server {
address: &str,
protocol_version: i32,
forge_mods: Vec<forge::ForgeMod>,
fml_network_version: Option<i64>,
) -> Result<Server, protocol::Error> {
let mut conn = protocol::Conn::new(address, protocol_version)?;
let tag = if !forge_mods.is_empty() {
"\0FML\0"
} else {
""
let tag = match fml_network_version {
Some(1) => "\0FML\0",
Some(2) => "\0FML2\0",
None => "",
_ => panic!("unsupported FML network version: {:?}", fml_network_version),
};
let host = conn.host.clone() + tag;
let port = conn.port;
conn.write_packet(protocol::packet::handshake::serverbound::Handshake {
@ -167,7 +170,6 @@ impl Server {
Some(rx),
));
}
// TODO: avoid duplication
protocol::packet::Packet::LoginSuccess_UUID(val) => {
warn!("Server is running in offline mode");
debug!("Login: {} {:?}", val.username, val.uuid);
@ -188,7 +190,7 @@ impl Server {
protocol::packet::Packet::LoginDisconnect(val) => {
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);
let uuid;
let compression_threshold = read.compression_threshold;
loop {
match read.read_packet()? {
protocol::packet::Packet::SetInitialCompression(val) => {
@ -248,7 +251,73 @@ impl Server {
protocol::packet::Packet::LoginDisconnect(val) => {
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) {
use crate::protocol::Serializable;
let mut buf: Vec<u8> = vec![];
msg.write_to(&mut buf).unwrap();
self.write_plugin_message("FML|HS", &buf);
let _ = self.conn.as_mut().unwrap().write_fmlhs_plugin_message(msg); // TODO handle errors
}
fn write_plugin_message(&mut self, channel: &str, data: &[u8]) {
if protocol::is_network_debug() {
debug!(
"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::<protocol::VarShort>::new(data.to_vec()),
});
}
let _ = self
.conn
.as_mut()
.unwrap()
.write_plugin_message(channel, data); // TODO handle errors
}
fn on_game_join_worldnames_ishard(