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 b27371a..946837c 100644 Binary files a/src/resources/registry_codec.nbt and b/src/resources/registry_codec.nbt differ