diff --git a/Cargo.lock b/Cargo.lock index 8e7e2eb..6baed31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ignore-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665ff4dce8edd10d490641ccb78949832f1ddbff02c584fb1f85ab888fe0e50c" + [[package]] name = "indexmap" version = "1.9.3" @@ -719,6 +725,7 @@ dependencies = [ "colog", "craftflow-nbt", "dashmap", + "ignore-result", "itertools", "log", "palette", diff --git a/Cargo.toml b/Cargo.toml index 94b4ff5..fb7e0c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ log = "0.4.27" uuid = "1.16.0" dashmap = "6.1.0" paste = "1.0.15" +ignore-result = "0.2.0" diff --git a/src/main.rs b/src/main.rs index 7586620..37eb0c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,12 @@ -use std::{env::args, io::Read, net::TcpListener, path::PathBuf, sync::Arc, thread, time::Duration}; +use std::{env::args, path::PathBuf, sync::Arc}; -use config::Config; -use event::{ConnectionState, Listener, PacketHandler}; -use context::{ClientContext, ServerContext}; use log::{debug, error, info}; -use player::{ClientInfo, Handshake, PlayerInfo}; -use rust_mc_proto::{DataReader, DataWriter, MinecraftConnection, Packet}; +use rust_mc_proto::Packet; +use server::{ + config::Config, context::ServerContext, data::text_component::TextComponent, event::{Listener, PacketHandler}, player::context::ClientContext, protocol::ConnectionState, start_server, ServerError +}; -use data::{ServerError, TextComponent}; -use pohuy::Pohuy; - -pub mod config; -pub mod data; -pub mod event; -pub mod context; -pub mod player; -pub mod pohuy; +pub mod server; struct ExampleListener; @@ -144,249 +135,6 @@ fn main() { // Бетонируем сервер контекст от изменений let server = Arc::new(server); - // Биндим сервер где надо - let Ok(listener) = TcpListener::bind(&server.config.bind.host) else { - error!("Не удалось забиндить сервер на {}", &server.config.bind.host); - return; - }; - - info!("Сервер запущен на {}", &server.config.bind.host); - - while let Ok((stream, addr)) = listener.accept() { - let server = server.clone(); - - thread::spawn(move || { - info!("Подключение: {}", addr); - - // Установка таймаутов на чтение и запись - // По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг - stream.set_read_timeout(Some(Duration::from_secs(server.config.bind.timeout))).pohuy(); - stream.set_write_timeout(Some(Duration::from_secs(server.config.bind.timeout))).pohuy(); - - // Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия - let conn = MinecraftConnection::new(stream); - - // Создаем контекст клиента - // Передавется во все листенеры и хандлеры чтобы определять именно этот клиент - let client = Arc::new(ClientContext::new(server.clone(), conn)); - - server.clients.insert(client.addr, client.clone()); - - // Обработка подключения - // Если ошибка -> выводим - match handle_connection(client.clone()) { - Ok(_) => {}, - Err(ServerError::ConnectionClosed) => {}, - Err(error) => { - error!("Ошибка подключения: {error:?}"); - }, - }; - - server.clients.remove(&client.addr); - - info!("Отключение: {}", addr); - }); - } -} - -fn handle_connection( - client: Arc, // Контекст клиента -) -> Result<(), ServerError> { - // Чтение рукопожатия - // Получение пакетов производится через client.conn(), - // ВАЖНО: не помещать сам client.conn() в переменные, - // он должен сразу убиваться иначе соединение гдето задедлочится - let mut packet = trigger_packet!(client.conn().read_packet()?, client, Handshake, incoming); - - if packet.id() != 0x00 { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет рукопожатия"))); - } // Айди пакета не рукопожатное - выходим из функции - - let protocol_version = packet.read_varint()?; // Получаем версия протокола, может быть отрицательным если наш клиент дэбил - let server_address = packet.read_string()?; // Получаем домен/адрес сервера к которому пытается подключиться клиент, например "play.example.com", а не айпи - let server_port = packet.read_unsigned_short()?; // Все тоже самое что и с адресом сервера и все потому же и за тем же - let next_state = packet.read_varint()?; // Тип подключения: 1 для получения статуса и пинга, 2 и 3 для обычного подключения - - // debug!("protocol_version: {protocol_version}"); - // debug!("server_address: {server_address}"); - // debug!("server_port: {server_port}"); - // debug!("next_state: {next_state}"); - - client.set_handshake(Handshake { protocol_version, server_address, server_port }); - - match next_state { - 1 => { // Тип подключения - статус - client.set_state(ConnectionState::Status)?; // Мы находимся в режиме Status - - loop { - // Чтение запроса - let packet = trigger_packet!(client.conn().read_packet()?, client, Status, incoming); - - match packet.id() { - 0x00 => { // Запрос статуса - let mut packet = Packet::empty(0x00); - - // Дефолтный статус - let mut status = "{ - \"version\": { - \"name\": \"Error\", - \"protocol\": 0 - }, - \"description\": {\"text\": \"Internal server error\"} - }".to_string(); - - // Опрос всех листенеров - for listener in client.server.listeners( // Цикл по листенерам - |o| o.on_status_priority() // Сортировка по приоритетности - ).iter() { - listener.on_status(client.clone(), &mut status)?; // Вызов метода листенера - } - - // Отправка статуса - packet.write_string(&status)?; - - client.conn().write_packet(&trigger_packet!(packet, client, Status, outcoming))?; - }, - 0x01 => { // Пинг - client.conn().write_packet(&trigger_packet!(packet, client, Status, outcoming))?; - // Просто отправляем этот же пакет обратно - // ID такой-же, содержание тоже, так почему бы и нет? - }, - _ => { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при чтении запросов статуса"))); - } - } - } - }, - 2 => { // Тип подключения - игра - client.set_state(ConnectionState::Login)?; // Мы находимся в режиме Login - - // Читаем пакет Login Start - let mut packet = trigger_packet!(client.conn().read_packet()?, client, Login, incoming); - - let name = packet.read_string()?; - let uuid = packet.read_uuid()?; - - // debug!("name: {name}"); - // debug!("uuid: {uuid}"); - - client.set_player_info(PlayerInfo { name: name.clone(), uuid: uuid.clone() }); - - if client.server.config.server.online_mode { - // TODO: encryption packets - } - - // Отправляем пакет Set Compression если сжатие указано - if let Some(threshold) = client.server.config.server.compression_threshold { - client.conn().write_packet(&trigger_packet!(Packet::build(0x03, |p| p.write_usize_varint(threshold))?, client, Login, outcoming))?; - client.conn().set_compression(Some(threshold)); // Устанавливаем сжатие на соединении - } - - // Отправка пакета Login Success - client.conn().write_packet(&trigger_packet!(Packet::build(0x02, |p| { - p.write_uuid(&uuid)?; - p.write_string(&name)?; - p.write_varint(0) - })?, client, Login, outcoming))?; - - let packet = trigger_packet!(client.conn().read_packet()?, client, Login, incoming); - - if packet.id() != 0x03 { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Login Acknowledged"))); - } - - client.set_state(ConnectionState::Configuration)?; // Мы перешли в режим Configuration - - // Получение бренда клиента из Serverbound Plugin Message - // Identifier канала откуда берется бренд: minecraft:brand - let brand = loop { - let mut packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); - - if packet.id() == 0x02 { // Пакет Serverbound Plugin Message - let identifier = packet.read_string()?; - - let mut data = Vec::new(); - packet.get_mut().read_to_end(&mut data).unwrap(); - - if identifier == "minecraft:brand" { - break String::from_utf8_lossy(&data).to_string(); - } else { - error!("unknown plugin message channel: {}", identifier); - } - } else { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Serverbound Plugin Message"))); - }; - }; - - // debug!("brand: {brand}"); - - let mut packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); - - // Пакет Client Information - if packet.id() != 0x00 { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Client Information"))); - } - - let locale = packet.read_string()?; // for example: ru_RU - let view_distance = packet.read_signed_byte()?; // client-side render distance in chunks - let chat_mode = packet.read_varint()?; // 0: enabled, 1: commands only, 2: hidden. See Chat#Client chat mode for more information. - let chat_colors = packet.read_boolean()?; // this settings does nothing on client but can be used on serverside - let displayed_skin_parts = packet.read_byte()?; // bit mask https://minecraft.wiki/w/Java_Edition_protocol#Client_Information_(configuration) - let main_hand = packet.read_varint()?; // 0 for left and 1 for right - let enable_text_filtering = packet.read_boolean()?; // filtering text for profanity, always false for offline mode - let allow_server_listings = packet.read_boolean()?; // allows showing player in server listings in status - let particle_status = packet.read_varint()?; // 0 for all, 1 for decreased, 2 for minimal - - // debug!("locale: {locale}"); - // debug!("view_distance: {view_distance}"); - // debug!("chat_mode: {chat_mode}"); - // debug!("chat_colors: {chat_colors}"); - // debug!("displayed_skin_parts: {displayed_skin_parts}"); - // debug!("main_hand: {main_hand}"); - // debug!("enable_text_filtering: {enable_text_filtering}"); - // debug!("allow_server_listings: {allow_server_listings}"); - // debug!("particle_status: {particle_status}"); - - client.set_client_info(ClientInfo { - brand, - locale, - view_distance, - chat_mode, - chat_colors, - displayed_skin_parts, - main_hand, - enable_text_filtering, - allow_server_listings, - particle_status - }); - - // TODO: Заюзать Listener'ы чтобы они подмешивали сюда чото - - client.conn().write_packet(&trigger_packet!(Packet::empty(0x03), client, Configuration, outcoming))?; - - let packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); - - if packet.id() != 0x03 { - return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Acknowledge Finish Configuration"))); - } - - client.set_state(ConnectionState::Play)?; // Мы перешли в режим Play - - // Отключение игрока с сообщением - // Отправляет в формате NBT TAG_String (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT#Specification:string_tag) - client.conn().write_packet(&trigger_packet!(Packet::build(0x1C, |p| { - let message = "server is in developmenet lol".to_string(); - p.write_byte(0x08)?; // NBT Type Name (TAG_String) - p.write_unsigned_short(message.len() as u16)?; // String length in unsigned short - p.write_bytes(message.as_bytes()) - })?, client, Play, outcoming))?; - - // TODO: Сделать отправку пакетов Play - }, - _ => { - return Err(ServerError::UnknownPacket(format!("Неизвестный NextState при рукопожатии"))); - } // Тип подключения не рукопожатный - } - - Ok(()) + // Запускаем сервер из специально отведенной под это дело функцией + start_server(server); } diff --git a/src/pohuy.rs b/src/pohuy.rs deleted file mode 100644 index d1a3902..0000000 --- a/src/pohuy.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub trait Pohuy: Sized { - fn pohuy(self) -> () {} -} - -impl Pohuy for Result {} \ No newline at end of file diff --git a/src/config.rs b/src/server/config.rs similarity index 100% rename from src/config.rs rename to src/server/config.rs diff --git a/src/server/context.rs b/src/server/context.rs new file mode 100644 index 0000000..678b152 --- /dev/null +++ b/src/server/context.rs @@ -0,0 +1,88 @@ +use std::{net::SocketAddr, sync::Arc}; + +use dashmap::DashMap; +use itertools::Itertools; +use uuid::Uuid; + +use super::{config::Config, event::{Listener, PacketHandler}, player::context::ClientContext}; + +pub struct ServerContext { + pub config: Arc, + pub clients: DashMap>, + listeners: Vec>, + handlers: Vec> +} + +impl ServerContext { + pub fn new(config: Arc) -> ServerContext { + ServerContext { + config, + listeners: Vec::new(), + handlers: Vec::new(), + clients: DashMap::new() + } + } + + pub fn get_player_by_uuid(self: &Arc, uuid: Uuid) -> Option> { + self.clients.iter() + .find(|o| { + let info = o.player_info(); + if let Some(info) = info { + info.uuid == uuid + } else { + false + } + }) + .map(|o| o.clone()) + } + + pub fn get_player_by_name(self: &Arc, name: &str) -> Option> { + self.clients.iter() + .find(|o| { + let info = o.player_info(); + if let Some(info) = info { + info.name == name + } else { + false + } + }) + .map(|o| o.clone()) + } + + pub fn players(self: &Arc) -> Vec> { + self.clients.iter() + .filter(|o| o.player_info().is_some()) + .map(|o| o.clone()) + .collect() + } + + pub fn add_packet_handler(&mut self, handler: Box) { + self.handlers.push(handler); + } + + pub fn add_listener(&mut self, listener: Box) { + self.listeners.push(listener); + } + + pub fn packet_handlers( + self: &Arc, + sort_by: F + ) -> Vec<&Box> + where + K: Ord, + F: FnMut(&&Box) -> K + { + self.handlers.iter().sorted_by_key(sort_by).collect_vec() + } + + pub fn listeners( + self: &Arc, + sort_by: F + ) -> Vec<&Box> + where + K: Ord, + F: FnMut(&&Box) -> K + { + self.listeners.iter().sorted_by_key(sort_by).collect_vec() + } +} diff --git a/src/server/data/mod.rs b/src/server/data/mod.rs new file mode 100644 index 0000000..62642bc --- /dev/null +++ b/src/server/data/mod.rs @@ -0,0 +1,2 @@ +pub mod text_component; + diff --git a/src/data.rs b/src/server/data/text_component.rs similarity index 81% rename from src/data.rs rename to src/server/data/text_component.rs index ac21304..36cac98 100644 --- a/src/data.rs +++ b/src/server/data/text_component.rs @@ -1,44 +1,14 @@ -use std::{error::Error, fmt::Display, io::Read}; -use palette::{Hsl, IntoColor, Srgb}; -use serde::{Deserialize, Serialize}; +use std::io::Read; -use rust_mc_proto::{DataReader, Packet, ProtocolError}; +use palette::{Hsl, IntoColor, Srgb}; +use rust_mc_proto::{DataReader, Packet}; +use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -// Ошибки сервера -#[derive(Debug)] -pub enum ServerError { - UnknownPacket(String), - Protocol(ProtocolError), - ConnectionClosed, - SerTextComponent, - DeTextComponent, - UnexpectedState -} +use crate::server::ServerError; -impl Display for ServerError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("{:?}", self)) - } -} -impl Error for ServerError {} -// Делаем чтобы ProtocolError мог переделываться в наш ServerError -impl From for ServerError { - fn from(error: ProtocolError) -> ServerError { - match error { - // Если просто закрыто соединение, переделываем в нашу ошибку этого - ProtocolError::ConnectionClosedError => { - ServerError::ConnectionClosed - }, - // Все остальное просто засовываем в обертку - error => { - ServerError::Protocol(error) - }, - } - } -} #[derive(Debug, Serialize, Deserialize, Clone)] #[skip_serializing_none] @@ -112,28 +82,6 @@ impl Default for TextComponent { } } -pub trait ReadWriteNBT: DataReader { - fn read_nbt(&mut self) -> Result; - fn write_nbt(&mut self, val: &T) -> Result<(), ServerError>; -} - -impl ReadWriteNBT for Packet { - fn read_nbt(&mut self) -> Result { - let mut data = Vec::new(); - let pos = self.get_ref().position(); - self.get_mut().read_to_end(&mut data).map_err(|_| ServerError::DeTextComponent)?; - let (remaining, value) = craftflow_nbt::from_slice(&data).map_err(|_| ServerError::DeTextComponent)?; - self.get_mut().set_position(pos + (data.len() - remaining.len()) as u64); - Ok(value) - } - - fn write_nbt(&mut self, val: &TextComponent) -> Result<(), ServerError> { - craftflow_nbt::to_writer(self.get_mut(), val) - .map_err(|_| ServerError::SerTextComponent)?; - Ok(()) - } -} - pub struct TextComponentBuilder { text: String, color: Option, @@ -211,4 +159,26 @@ impl TextComponentBuilder { extra: self.extra } } +} + +pub trait ReadWriteNBT: DataReader { + fn read_nbt(&mut self) -> Result; + fn write_nbt(&mut self, val: &T) -> Result<(), ServerError>; +} + +impl ReadWriteNBT for Packet { + fn read_nbt(&mut self) -> Result { + let mut data = Vec::new(); + let pos = self.get_ref().position(); + self.get_mut().read_to_end(&mut data).map_err(|_| ServerError::DeTextComponent)?; + let (remaining, value) = craftflow_nbt::from_slice(&data).map_err(|_| ServerError::DeTextComponent)?; + self.get_mut().set_position(pos + (data.len() - remaining.len()) as u64); + Ok(value) + } + + fn write_nbt(&mut self, val: &TextComponent) -> Result<(), ServerError> { + craftflow_nbt::to_writer(self.get_mut(), val) + .map_err(|_| ServerError::SerTextComponent)?; + Ok(()) + } } \ No newline at end of file diff --git a/src/event.rs b/src/server/event/mod.rs similarity index 87% rename from src/event.rs rename to src/server/event/mod.rs index 1682f05..5b1526e 100644 --- a/src/event.rs +++ b/src/server/event/mod.rs @@ -1,6 +1,6 @@ use rust_mc_proto::Packet; -use crate::{context::ClientContext, data::ServerError}; +use super::protocol::ConnectionState; #[macro_export] macro_rules! generate_handlers { @@ -10,7 +10,7 @@ macro_rules! generate_handlers { 0 } - fn [](&self, _: std::sync::Arc $(, _: $arg_ty)*) -> Result<(), ServerError> { + fn [](&self, _: std::sync::Arc $(, _: $arg_ty)*) -> Result<(), crate::server::ServerError> { Ok(()) } } @@ -26,7 +26,7 @@ macro_rules! trigger_packet { for handler in $client.server.packet_handlers( |o| o.[]() ).iter() { - handler.[]($client.clone(), &mut packet, crate::event::ConnectionState::$state)?; + handler.[]($client.clone(), &mut packet, crate::server::protocol::ConnectionState::$state)?; } packet.get_mut().set_position(0); packet @@ -89,13 +89,4 @@ pub trait PacketHandler: Sync + Send { generate_handlers!(incoming_packet, &mut Packet, ConnectionState); generate_handlers!(outcoming_packet, &mut Packet, ConnectionState); generate_handlers!(state, ConnectionState); -} - -#[derive(Debug, Clone)] -pub enum ConnectionState { - Handshake, - Status, - Login, - Configuration, - Play } \ No newline at end of file diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..f8c6510 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,96 @@ +use std::{error::Error, fmt::Display, net::TcpListener, sync::Arc, thread, time::Duration}; + +use context::ServerContext; +use ignore_result::Ignore; +use log::{error, info}; +use player::context::ClientContext; +use protocol::handle_connection; +use rust_mc_proto::{MinecraftConnection, ProtocolError}; + +pub mod config; +pub mod data; +pub mod event; +pub mod player; +pub mod context; +pub mod protocol; + +// Ошибки сервера +#[derive(Debug)] +pub enum ServerError { + UnknownPacket(String), + Protocol(ProtocolError), + ConnectionClosed, + SerTextComponent, + DeTextComponent, + UnexpectedState +} + +impl Display for ServerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{:?}", self)) + } +} + +impl Error for ServerError {} + +// Делаем чтобы ProtocolError мог переделываться в наш ServerError +impl From for ServerError { + fn from(error: ProtocolError) -> ServerError { + match error { + // Если просто закрыто соединение, переделываем в нашу ошибку этого + ProtocolError::ConnectionClosedError => { + ServerError::ConnectionClosed + }, + // Все остальное просто засовываем в обертку + error => { + ServerError::Protocol(error) + }, + } + } +} + +pub fn start_server(server: Arc) { + // Биндим сервер где надо + let Ok(listener) = TcpListener::bind(&server.config.bind.host) else { + error!("Не удалось забиндить сервер на {}", &server.config.bind.host); + return; + }; + + info!("Сервер запущен на {}", &server.config.bind.host); + + while let Ok((stream, addr)) = listener.accept() { + let server = server.clone(); + + thread::spawn(move || { + info!("Подключение: {}", addr); + + // Установка таймаутов на чтение и запись + // По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг + stream.set_read_timeout(Some(Duration::from_secs(server.config.bind.timeout))).ignore(); + stream.set_write_timeout(Some(Duration::from_secs(server.config.bind.timeout))).ignore(); + + // Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия + let conn = MinecraftConnection::new(stream); + + // Создаем контекст клиента + // Передавется во все листенеры и хандлеры чтобы определять именно этот клиент + let client = Arc::new(ClientContext::new(server.clone(), conn)); + + server.clients.insert(client.addr, client.clone()); + + // Обработка подключения + // Если ошибка -> выводим + match handle_connection(client.clone()) { + Ok(_) => {}, + Err(ServerError::ConnectionClosed) => {}, + Err(error) => { + error!("Ошибка подключения: {error:?}"); + }, + }; + + server.clients.remove(&client.addr); + + info!("Отключение: {}", addr); + }); + } +} diff --git a/src/context.rs b/src/server/player/context.rs similarity index 53% rename from src/context.rs rename to src/server/player/context.rs index 9519783..e04385f 100644 --- a/src/context.rs +++ b/src/server/player/context.rs @@ -1,92 +1,12 @@ use std::{hash::Hash, net::{SocketAddr, TcpStream}, sync::{Arc, RwLock, RwLockWriteGuard}}; -use dashmap::DashMap; -use itertools::Itertools; use rust_mc_proto::MinecraftConnection; use uuid::Uuid; -use crate::{config::Config, data::ServerError, event::{ConnectionState, Listener, PacketHandler}, player::{ClientInfo, Handshake, PlayerInfo, ProtocolHelper}}; +use crate::server::{context::ServerContext, protocol::ConnectionState, ServerError}; -pub struct ServerContext { - pub config: Arc, - pub clients: DashMap>, - listeners: Vec>, - handlers: Vec> -} +use super::protocol::ProtocolHelper; -impl ServerContext { - pub fn new(config: Arc) -> ServerContext { - ServerContext { - config, - listeners: Vec::new(), - handlers: Vec::new(), - clients: DashMap::new() - } - } - - pub fn get_player_by_uuid(self: &Arc, uuid: Uuid) -> Option> { - self.clients.iter() - .find(|o| { - let info = o.player_info(); - if let Some(info) = info { - info.uuid == uuid - } else { - false - } - }) - .map(|o| o.clone()) - } - - pub fn get_player_by_name(self: &Arc, name: &str) -> Option> { - self.clients.iter() - .find(|o| { - let info = o.player_info(); - if let Some(info) = info { - info.name == name - } else { - false - } - }) - .map(|o| o.clone()) - } - - pub fn players(self: &Arc) -> Vec> { - self.clients.iter() - .filter(|o| o.player_info().is_some()) - .map(|o| o.clone()) - .collect() - } - - pub fn add_packet_handler(&mut self, handler: Box) { - self.handlers.push(handler); - } - - pub fn add_listener(&mut self, listener: Box) { - self.listeners.push(listener); - } - - pub fn packet_handlers( - self: &Arc, - sort_by: F - ) -> Vec<&Box> - where - K: Ord, - F: FnMut(&&Box) -> K - { - self.handlers.iter().sorted_by_key(sort_by).collect_vec() - } - - pub fn listeners( - self: &Arc, - sort_by: F - ) -> Vec<&Box> - where - K: Ord, - F: FnMut(&&Box) -> K - { - self.listeners.iter().sorted_by_key(sort_by).collect_vec() - } -} pub struct ClientContext { pub server: Arc, @@ -175,4 +95,31 @@ impl ClientContext { pub fn protocol_helper(self: &Arc) -> ProtocolHelper { ProtocolHelper::new(self.clone()) } -} \ No newline at end of file +} + +#[derive(Clone)] +pub struct Handshake { + pub protocol_version: i32, + pub server_address: String, + pub server_port: u16, +} + +#[derive(Clone)] +pub struct ClientInfo { + pub brand: String, + pub locale: String, + pub view_distance: i8, + pub chat_mode: i32, + pub chat_colors: bool, + pub displayed_skin_parts: u8, + pub main_hand: i32, + pub enable_text_filtering: bool, + pub allow_server_listings: bool, + pub particle_status: i32 +} + +#[derive(Clone)] +pub struct PlayerInfo { + pub name: String, + pub uuid: Uuid +} diff --git a/src/server/player/mod.rs b/src/server/player/mod.rs new file mode 100644 index 0000000..922da05 --- /dev/null +++ b/src/server/player/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod protocol; \ No newline at end of file diff --git a/src/player.rs b/src/server/player/protocol.rs similarity index 83% rename from src/player.rs rename to src/server/player/protocol.rs index 91c09cd..adbd67c 100644 --- a/src/player.rs +++ b/src/server/player/protocol.rs @@ -1,36 +1,11 @@ use std::{io::Read, sync::Arc}; use rust_mc_proto::{DataReader, DataWriter, Packet}; -use uuid::Uuid; -use crate::{context::ClientContext, data::{ServerError, TextComponent, ReadWriteNBT}, event::ConnectionState}; +use crate::server::{data::text_component::{ReadWriteNBT, TextComponent}, protocol::ConnectionState, ServerError}; -#[derive(Clone)] -pub struct Handshake { - pub protocol_version: i32, - pub server_address: String, - pub server_port: u16, -} +use super::context::ClientContext; -#[derive(Clone)] -pub struct ClientInfo { - pub brand: String, - pub locale: String, - pub view_distance: i8, - pub chat_mode: i32, - pub chat_colors: bool, - pub displayed_skin_parts: u8, - pub main_hand: i32, - pub enable_text_filtering: bool, - pub allow_server_listings: bool, - pub particle_status: i32 -} - -#[derive(Clone)] -pub struct PlayerInfo { - pub name: String, - pub uuid: Uuid -} pub struct ProtocolHelper { client: Arc, diff --git a/src/server/protocol.rs b/src/server/protocol.rs new file mode 100644 index 0000000..1bfb11c --- /dev/null +++ b/src/server/protocol.rs @@ -0,0 +1,218 @@ +use std::{io::Read, sync::Arc}; + +use super::{player::context::{ClientContext, ClientInfo, Handshake, PlayerInfo}, ServerError}; +use log::error; +use rust_mc_proto::{DataReader, DataWriter, Packet}; + +use crate::trigger_packet; + +#[derive(Debug, Clone)] +pub enum ConnectionState { + Handshake, + Status, + Login, + Configuration, + Play +} + +pub fn handle_connection( + client: Arc, // Контекст клиента +) -> Result<(), ServerError> { + // Чтение рукопожатия + // Получение пакетов производится через client.conn(), + // ВАЖНО: не помещать сам client.conn() в переменные, + // он должен сразу убиваться иначе соединение гдето задедлочится + let mut packet = trigger_packet!(client.conn().read_packet()?, client, Handshake, incoming); + + if packet.id() != 0x00 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет рукопожатия"))); + } // Айди пакета не рукопожатное - выходим из функции + + let protocol_version = packet.read_varint()?; // Получаем версия протокола, может быть отрицательным если наш клиент дэбил + let server_address = packet.read_string()?; // Получаем домен/адрес сервера к которому пытается подключиться клиент, например "play.example.com", а не айпи + let server_port = packet.read_unsigned_short()?; // Все тоже самое что и с адресом сервера и все потому же и за тем же + let next_state = packet.read_varint()?; // Тип подключения: 1 для получения статуса и пинга, 2 и 3 для обычного подключения + + // debug!("protocol_version: {protocol_version}"); + // debug!("server_address: {server_address}"); + // debug!("server_port: {server_port}"); + // debug!("next_state: {next_state}"); + + client.set_handshake(Handshake { protocol_version, server_address, server_port }); + + match next_state { + 1 => { // Тип подключения - статус + client.set_state(ConnectionState::Status)?; // Мы находимся в режиме Status + + loop { + // Чтение запроса + let packet = trigger_packet!(client.conn().read_packet()?, client, Status, incoming); + + match packet.id() { + 0x00 => { // Запрос статуса + let mut packet = Packet::empty(0x00); + + // Дефолтный статус + let mut status = "{ + \"version\": { + \"name\": \"Error\", + \"protocol\": 0 + }, + \"description\": {\"text\": \"Internal server error\"} + }".to_string(); + + // Опрос всех листенеров + for listener in client.server.listeners( // Цикл по листенерам + |o| o.on_status_priority() // Сортировка по приоритетности + ).iter() { + listener.on_status(client.clone(), &mut status)?; // Вызов метода листенера + } + + // Отправка статуса + packet.write_string(&status)?; + + client.conn().write_packet(&trigger_packet!(packet, client, Status, outcoming))?; + }, + 0x01 => { // Пинг + client.conn().write_packet(&trigger_packet!(packet, client, Status, outcoming))?; + // Просто отправляем этот же пакет обратно + // ID такой-же, содержание тоже, так почему бы и нет? + }, + _ => { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при чтении запросов статуса"))); + } + } + } + }, + 2 => { // Тип подключения - игра + client.set_state(ConnectionState::Login)?; // Мы находимся в режиме Login + + // Читаем пакет Login Start + let mut packet = trigger_packet!(client.conn().read_packet()?, client, Login, incoming); + + let name = packet.read_string()?; + let uuid = packet.read_uuid()?; + + // debug!("name: {name}"); + // debug!("uuid: {uuid}"); + + client.set_player_info(PlayerInfo { name: name.clone(), uuid: uuid.clone() }); + + if client.server.config.server.online_mode { + // TODO: encryption packets + } + + // Отправляем пакет Set Compression если сжатие указано + if let Some(threshold) = client.server.config.server.compression_threshold { + client.conn().write_packet(&trigger_packet!(Packet::build(0x03, |p| p.write_usize_varint(threshold))?, client, Login, outcoming))?; + client.conn().set_compression(Some(threshold)); // Устанавливаем сжатие на соединении + } + + // Отправка пакета Login Success + client.conn().write_packet(&trigger_packet!(Packet::build(0x02, |p| { + p.write_uuid(&uuid)?; + p.write_string(&name)?; + p.write_varint(0) + })?, client, Login, outcoming))?; + + let packet = trigger_packet!(client.conn().read_packet()?, client, Login, incoming); + + if packet.id() != 0x03 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Login Acknowledged"))); + } + + client.set_state(ConnectionState::Configuration)?; // Мы перешли в режим Configuration + + // Получение бренда клиента из Serverbound Plugin Message + // Identifier канала откуда берется бренд: minecraft:brand + let brand = loop { + let mut packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); + + if packet.id() == 0x02 { // Пакет Serverbound Plugin Message + let identifier = packet.read_string()?; + + let mut data = Vec::new(); + packet.get_mut().read_to_end(&mut data).unwrap(); + + if identifier == "minecraft:brand" { + break String::from_utf8_lossy(&data).to_string(); + } else { + error!("unknown plugin message channel: {}", identifier); + } + } else { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Serverbound Plugin Message"))); + }; + }; + + // debug!("brand: {brand}"); + + let mut packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); + + // Пакет Client Information + if packet.id() != 0x00 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Client Information"))); + } + + let locale = packet.read_string()?; // for example: ru_RU + let view_distance = packet.read_signed_byte()?; // client-side render distance in chunks + let chat_mode = packet.read_varint()?; // 0: enabled, 1: commands only, 2: hidden. See Chat#Client chat mode for more information. + let chat_colors = packet.read_boolean()?; // this settings does nothing on client but can be used on serverside + let displayed_skin_parts = packet.read_byte()?; // bit mask https://minecraft.wiki/w/Java_Edition_protocol#Client_Information_(configuration) + let main_hand = packet.read_varint()?; // 0 for left and 1 for right + let enable_text_filtering = packet.read_boolean()?; // filtering text for profanity, always false for offline mode + let allow_server_listings = packet.read_boolean()?; // allows showing player in server listings in status + let particle_status = packet.read_varint()?; // 0 for all, 1 for decreased, 2 for minimal + + // debug!("locale: {locale}"); + // debug!("view_distance: {view_distance}"); + // debug!("chat_mode: {chat_mode}"); + // debug!("chat_colors: {chat_colors}"); + // debug!("displayed_skin_parts: {displayed_skin_parts}"); + // debug!("main_hand: {main_hand}"); + // debug!("enable_text_filtering: {enable_text_filtering}"); + // debug!("allow_server_listings: {allow_server_listings}"); + // debug!("particle_status: {particle_status}"); + + client.set_client_info(ClientInfo { + brand, + locale, + view_distance, + chat_mode, + chat_colors, + displayed_skin_parts, + main_hand, + enable_text_filtering, + allow_server_listings, + particle_status + }); + + // TODO: Заюзать Listener'ы чтобы они подмешивали сюда чото + + client.conn().write_packet(&trigger_packet!(Packet::empty(0x03), client, Configuration, outcoming))?; + + let packet = trigger_packet!(client.conn().read_packet()?, client, Configuration, incoming); + + if packet.id() != 0x03 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Acknowledge Finish Configuration"))); + } + + client.set_state(ConnectionState::Play)?; // Мы перешли в режим Play + + // Отключение игрока с сообщением + // Отправляет в формате NBT TAG_String (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT#Specification:string_tag) + client.conn().write_packet(&trigger_packet!(Packet::build(0x1C, |p| { + let message = "server is in developmenet lol".to_string(); + p.write_byte(0x08)?; // NBT Type Name (TAG_String) + p.write_unsigned_short(message.len() as u16)?; // String length in unsigned short + p.write_bytes(message.as_bytes()) + })?, client, Play, outcoming))?; + + // TODO: Сделать отправку пакетов Play + }, + _ => { + return Err(ServerError::UnknownPacket(format!("Неизвестный NextState при рукопожатии"))); + } // Тип подключения не рукопожатный + } + + Ok(()) +}