From d4cf71821469a8ef533dd65c6c81b18ea61df8e4 Mon Sep 17 00:00:00 2001 From: TriMill Date: Sat, 17 Dec 2022 15:39:56 -0500 Subject: [PATCH] added velocity support --- Cargo.lock | 83 +++++++++++++++++++++++ Cargo.toml | 2 + src/config.rs | 14 +++- src/main.rs | 8 +-- src/network/client.rs | 2 +- src/network/server.rs | 113 ++++++++++++++++++++++++------- src/protocol/clientbound.rs | 7 ++ src/protocol/data.rs | 12 +++- src/protocol/serverbound.rs | 15 +++- src/resources/registry_codec.nbt | Bin 24946 -> 26831 bytes 10 files changed, 221 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32b63f9..327439f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "0.2.17" @@ -108,6 +117,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -117,6 +135,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cxx" version = "1.0.83" @@ -161,6 +189,17 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "either" version = "1.8.0" @@ -220,6 +259,16 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "hematite-nbt" version = "0.5.2" @@ -241,6 +290,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "humantime" version = "2.1.0" @@ -465,10 +523,12 @@ dependencies = [ "chrono", "env_logger", "hematite-nbt", + "hmac", "log", "mlua", "serde", "serde_json", + "sha2", "uuid", ] @@ -561,6 +621,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.105" @@ -592,6 +669,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-ident" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 197d20f..333ca5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,5 @@ uuid = "1.2" log = "0.4.0" env_logger = "0.10.0" chrono = "0.4" +hmac = "0.12" +sha2 = "0.10" diff --git a/src/config.rs b/src/config.rs index 1273139..5434b46 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,13 +2,25 @@ use std::{net::IpAddr, fs::OpenOptions}; use serde::Deserialize; +#[derive(Deserialize, PartialEq, Eq)] +pub enum LoginMode { + Offline, + Velocity, + // TODO online, bungeecord +} + #[derive(Deserialize)] pub struct Config { pub addr: IpAddr, pub port: u16, + pub login: LoginMode, + pub velocity_secret: Option, } pub fn load_config() -> Result> { - let config = serde_json::from_reader(OpenOptions::new().read(true).open("./config.json")?)?; + let config: Config = serde_json::from_reader(OpenOptions::new().read(true).open("./config.json")?)?; + if config.login == LoginMode::Velocity && config.velocity_secret.is_none() { + Err("Velocity is enabled but no secret is configured")? + } Ok(config) } diff --git a/src/main.rs b/src/main.rs index 2473004..8291bea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::net::SocketAddr; use std::time::Duration; use std::io::Write; @@ -40,18 +39,17 @@ fn main() { writeln!(buf, "\x1b[90m[\x1b[37m{} {color}{}\x1b[37m {}\x1b[90m]\x1b[0m {}", now, record.level(), target, record.args()) }).init(); + + info!("Starting Quectocraft version {}", VERSION); let config = load_config().expect("Failed to load config"); - let addr = SocketAddr::new(config.addr, config.port); - - info!("Starting Quectocraft version {}", VERSION); let lua = Lua::new(); let mut plugins = Plugins::new(&lua).expect("Error initializing lua environment"); std::fs::create_dir_all("plugins").expect("Couldn't create the plugins directory"); plugins.load_plugins(); - let mut server = NetworkServer::new(addr, plugins); + let mut server = NetworkServer::new(config, plugins); let sleep_dur = Duration::from_millis(5); let mut i = 0; loop { diff --git a/src/network/client.rs b/src/network/client.rs index eb7bd0d..48d3a38 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -8,7 +8,7 @@ use super::Player; pub struct NetworkClient { pub id: i32, - pub play: bool, + pub verified: bool, pub closed: bool, pub stream: TcpStream, pub serverbound: Receiver, diff --git a/src/network/server.rs b/src/network/server.rs index 0e76989..9d40b7b 100644 --- a/src/network/server.rs +++ b/src/network/server.rs @@ -1,9 +1,11 @@ use std::{net::{TcpListener, SocketAddr}, thread, sync::mpsc::{Receiver, Sender, channel}, collections::HashSet}; -use log::{info, warn, debug}; +use hmac::{Hmac, Mac}; +use log::{info, warn, debug, trace}; use serde_json::json; +use sha2::Sha256; -use crate::protocol::{data::PacketEncoder, serverbound::*, clientbound::*, command::Commands, Position}; +use crate::{protocol::{data::PacketEncoder, serverbound::*, clientbound::*, command::Commands, Position}, config::{Config, LoginMode}}; use crate::plugins::{Plugins, Response}; use crate::VERSION; @@ -14,18 +16,20 @@ pub struct NetworkServer<'lua> { commands: Commands, new_clients: Receiver, clients: Vec, + config: Config, } impl <'lua> NetworkServer<'lua> { - pub fn new(addr: SocketAddr, mut plugins: Plugins<'lua>) -> Self { + pub fn new(config: Config, mut plugins: Plugins<'lua>) -> Self { let (send, recv) = channel(); info!("Initializing plugins"); plugins.init(); let mut commands = Commands::new(); commands.create_simple_cmd("qc"); let commands = plugins.register_commands(commands).unwrap(); - thread::spawn(move || Self::listen(&addr, send)); - Self { + thread::spawn(move || Self::listen(&SocketAddr::new(config.addr, config.port), send)); + Self { + config, plugins, commands, new_clients: recv, @@ -44,7 +48,7 @@ impl <'lua> NetworkServer<'lua> { thread::spawn(|| NetworkClient::listen(stream_2, send)); let client = NetworkClient { id: id as i32, - play: false, + verified: false, closed: false, stream, serverbound: recv, @@ -63,13 +67,11 @@ impl <'lua> NetworkServer<'lua> { pub fn send_keep_alive(&mut self) { let mut closed = Vec::new(); for client in self.clients.iter_mut() { - if client.play { + if client.player.is_some() { let result = client.send_packet(ClientBoundPacket::KeepAlive(0)); if result.is_err() { client.close(); - if let Some(pl) = &client.player { - self.plugins.player_leave(pl); - } + self.plugins.player_leave(client.player.as_ref().unwrap()); closed.push(client.id); } } @@ -87,6 +89,7 @@ impl <'lua> NetworkServer<'lua> { while let Some(packet) = client.recv_packet(&mut alive) { let result = self.handle_packet(client, packet); if result.is_err() { + warn!("error: {}", result.unwrap_err()); alive = false; break } @@ -102,7 +105,7 @@ impl <'lua> NetworkServer<'lua> { for response in self.plugins.get_responses() { let _ = self.handle_plugin_response(response); } - self.clients.retain(|x| !closed.contains(&x.id)); + self.clients.retain(|x| !closed.contains(&x.id) && !x.closed); } fn handle_plugin_response(&mut self, response: Response) -> std::io::Result<()> { @@ -137,7 +140,8 @@ impl <'lua> NetworkServer<'lua> { Ok(()) } - fn handle_packet(&mut self, client: &mut NetworkClient, packet: ServerBoundPacket) -> std::io::Result<()> { + fn handle_packet(&mut self, client: &mut NetworkClient, packet: ServerBoundPacket) -> Result<(), Box> { + trace!("Recieved packet from client {}:", client.id); match packet { ServerBoundPacket::Ignored(_) => (), ServerBoundPacket::Unknown(id) => warn!("Unknown packet: {}", id), @@ -152,21 +156,80 @@ impl <'lua> NetworkServer<'lua> { } ServerBoundPacket::LoginStart(login_start) => { if self.clients.iter().filter_map(|x| x.player.as_ref()).any(|x| x.uuid == login_start.uuid) { - client.send_packet(ClientBoundPacket::LoginDisconnect(json!({"translate": "multiplayer.disconnect.duplicate_login"})))?; + client.send_packet(ClientBoundPacket::LoginDisconnect(json!({ + "translate": "multiplayer.disconnect.duplicate_login" + })))?; client.close(); - } else { - client.player = Some(Player { - name: login_start.name.clone(), - uuid: login_start.uuid, - }); - client.play = true; - client.send_packet(ClientBoundPacket::LoginSuccess(LoginSuccess { - name: login_start.name, - uuid: login_start.uuid, - }))?; - self.plugins.player_join(client.player.as_ref().unwrap()); - self.post_login(client)?; + return Ok(()) } + client.player = Some(Player { + name: login_start.name.clone(), + uuid: login_start.uuid, + }); + match self.config.login { + LoginMode::Offline => { + client.verified = true; + client.send_packet(ClientBoundPacket::LoginPluginRequest{ + id: -1, + channel: "qc:init".to_owned(), + data: Vec::new() + })?; + }, + LoginMode::Velocity => { + client.send_packet(ClientBoundPacket::LoginPluginRequest{ + id: 10, + channel: "velocity:player_info".to_owned(), + data: vec![1], + })? + } + } + } + // Finalize login + ServerBoundPacket::LoginPluginResponse { id: -1, .. } => { + if !client.verified { + client.send_packet(ClientBoundPacket::Disconnect(json!({ + "text": "Failed to verify your connection", + "color": "red", + })))?; + client.close(); + } + client.send_packet(ClientBoundPacket::LoginSuccess(LoginSuccess { + name: client.player.as_ref().unwrap().name.to_owned(), + uuid: client.player.as_ref().unwrap().uuid, + }))?; + self.plugins.player_join(client.player.as_ref().unwrap()); + self.post_login(client)?; + } + // Velocity login + ServerBoundPacket::LoginPluginResponse { id: 10, data } => { + let Some(data) = data else { + client.send_packet(ClientBoundPacket::LoginDisconnect(json!({ + "text": "This server can only be connected to via a Velocity proxy", + "color": "red" + })))?; + client.close(); + return Ok(()); + }; + let (sig, data) = data.split_at(32); + let mut mac = Hmac::::new_from_slice(self.config.velocity_secret.clone().unwrap().as_bytes())?; + mac.update(data); + if let Err(_) = mac.verify_slice(sig) { + client.send_packet(ClientBoundPacket::Disconnect(json!({ + "text": "Could not verify secret. Ensure that the secrets configured for Velocity and Quectocraft match." + })))?; + client.close(); + return Ok(()) + } + client.verified = true; + client.send_packet(ClientBoundPacket::LoginPluginRequest{ + id: -1, + channel: "qc:init".to_owned(), + data: Vec::new() + })?; + } + ServerBoundPacket::LoginPluginResponse { .. } => { + client.send_packet(ClientBoundPacket::LoginDisconnect(json!({"text": "bad plugin response"})))?; + client.close(); } ServerBoundPacket::ChatMessage(msg) => { self.plugins.chat_message(client.player.as_ref().unwrap(), &msg.message); diff --git a/src/protocol/clientbound.rs b/src/protocol/clientbound.rs index 7f5dffb..ca02404 100644 --- a/src/protocol/clientbound.rs +++ b/src/protocol/clientbound.rs @@ -142,6 +142,7 @@ pub enum ClientBoundPacket { StatusResponse(String), PingResponse(i64), // login + LoginPluginRequest { id: i32, channel: String, data: Vec }, LoginSuccess(LoginSuccess), LoginDisconnect(serde_json::Value), // play @@ -175,6 +176,12 @@ impl ClientBoundPacket { packet.write_string(262144, &message.to_string()); finalize_packet(packet, 0) } + Self::LoginPluginRequest { id, channel, data } => { + packet.write_varint(id); + packet.write_string(32767, &channel); + packet.write_bytes(&data); + finalize_packet(packet, 4) + } Self::LoginSuccess(login_success) => { login_success.encode(&mut packet); finalize_packet(packet, 2) diff --git a/src/protocol/data.rs b/src/protocol/data.rs index 346c919..ce436cf 100644 --- a/src/protocol/data.rs +++ b/src/protocol/data.rs @@ -50,7 +50,8 @@ pub trait PacketEncoder: Write { self.write_all(&data.to_be_bytes()).unwrap(); } - fn write_varint(&mut self, mut data: i32) { + fn write_varint(&mut self, data: i32) { + let mut data = data as u32; loop { let mut byte = (data & 0b11111111) as u8; data >>= 7; @@ -64,7 +65,8 @@ pub trait PacketEncoder: Write { } } - fn write_varlong(&mut self, mut data: i64) { + fn write_varlong(&mut self, data: i64) { + let mut data = data as u64; loop { let mut byte = (data & 0b11111111) as u8; data >>= 7; @@ -161,6 +163,12 @@ impl PacketDecoder { ret } + pub fn read_to_end(&mut self) -> &[u8] { + let ret = &self.data[self.idx..]; + self.idx = self.data.len(); + ret + } + pub fn read_bool(&mut self) -> bool { self.idx += 1; self.data[self.idx-1] != 0 diff --git a/src/protocol/serverbound.rs b/src/protocol/serverbound.rs index a7aa9dc..38fe448 100644 --- a/src/protocol/serverbound.rs +++ b/src/protocol/serverbound.rs @@ -71,6 +71,7 @@ pub enum ServerBoundPacket { PingRequest(i64), // login LoginStart(LoginStart), + LoginPluginResponse { id: i32, data: Option> }, // play ChatMessage(ChatMessage), ChatCommand(ChatMessage), @@ -94,9 +95,21 @@ impl ServerBoundPacket { (NS::Status, 1) => ServerBoundPacket::PingRequest(decoder.read_long()), (NS::Login, 0) => { - *state = NS::Play; ServerBoundPacket::LoginStart(LoginStart::decode(decoder)) }, + (NS::Login, 2) => { + let id = decoder.read_varint(); + let success = decoder.read_bool(); + let data = if success { + Some(decoder.read_to_end().to_vec()) + } else { + None + }; + if id == -1 { + *state = NetworkState::Play; + } + ServerBoundPacket::LoginPluginResponse { id, data } + }, (NS::Play, 4) => ServerBoundPacket::ChatCommand(ChatMessage::decode(decoder)), (NS::Play, 5) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)), (NS::Play, id @ (17 | 19 | 20 | 21 | 29)) => ServerBoundPacket::Ignored(id), diff --git a/src/resources/registry_codec.nbt b/src/resources/registry_codec.nbt index b27371a82fbf053e9650cd4e6e8b4633d2154e14..946837c6d1ac97741878afed3e9602e9a3546275 100644 GIT binary patch literal 26831 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 By_x_3 literal 24946 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