add a thread for internal server IO and implement account checking with it
authoralfadur
Tue, 09 Apr 2019 21:08:35 +0300
changeset 14784 f43ab2bd76ae
parent 14783 bbec6b28d072
child 14785 65861ba8b4e8
add a thread for internal server IO and implement account checking with it
rust/hedgewars-server/Cargo.toml
rust/hedgewars-server/src/main.rs
rust/hedgewars-server/src/protocol/messages.rs
rust/hedgewars-server/src/server.rs
rust/hedgewars-server/src/server/client.rs
rust/hedgewars-server/src/server/core.rs
rust/hedgewars-server/src/server/database.rs
rust/hedgewars-server/src/server/handlers.rs
rust/hedgewars-server/src/server/handlers/loggingin.rs
rust/hedgewars-server/src/server/io.rs
rust/hedgewars-server/src/server/network.rs
rust/hedgewars-server/src/utils.rs
--- a/rust/hedgewars-server/Cargo.toml	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/Cargo.toml	Tue Apr 09 21:08:35 2019 +0300
@@ -12,6 +12,7 @@
 [dependencies]
 rand = "0.6"
 mio = "0.6"
+mio-extras = "2.0.5"
 slab = "0.4"
 netbuf = "0.4"
 nom = { git = "https://github.com/Geal/nom", branch = "5.0" }
--- a/rust/hedgewars-server/src/main.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/main.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -40,13 +40,16 @@
         for event in events.iter() {
             if event.readiness() & Ready::readable() == Ready::readable() {
                 match event.token() {
-                    utils::SERVER => hw_network.accept_client(&poll).unwrap(),
+                    utils::SERVER_TOKEN => hw_network.accept_client(&poll).unwrap(),
+                    #[cfg(feature = "official-server")]
+                    utils::IO_TOKEN => hw_network.handle_io_result(),
                     Token(tok) => hw_network.client_readable(&poll, tok).unwrap(),
                 }
             }
             if event.readiness() & Ready::writable() == Ready::writable() {
                 match event.token() {
-                    utils::SERVER => unreachable!(),
+                    utils::SERVER_TOKEN => unreachable!(),
+                    utils::IO_TOKEN => unreachable!(),
                     Token(tok) => hw_network.client_writable(&poll, tok).unwrap(),
                 }
             }
--- a/rust/hedgewars-server/src/protocol/messages.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/protocol/messages.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -71,8 +71,11 @@
     Ping,
     Pong,
     Bye(String),
+
     Nick(String),
     Proto(u16),
+    AskPassword(String),
+
     ServerAuth(String),
     LobbyLeft(String, String),
     LobbyJoined(Vec<String>),
@@ -282,6 +285,7 @@
             Bye(msg) => msg!["BYE", msg],
             Nick(nick) => msg!["NICK", nick],
             Proto(proto) => msg!["PROTO", proto],
+            AskPassword(salt) => msg!["ASKPASSWORD", salt],
             ServerAuth(hash) => msg!["SERVER_AUTH", hash],
             LobbyLeft(nick, msg) => msg!["LOBBY:LEFT", nick, msg],
             LobbyJoined(nicks) => construct_message(&["LOBBY:JOINED"], &nicks),
--- a/rust/hedgewars-server/src/server.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -6,6 +6,7 @@
 mod database;
 mod handlers;
 pub mod indexslab;
+#[cfg(feature = "official-server")]
 pub mod io;
 pub mod network;
 pub mod room;
--- a/rust/hedgewars-server/src/server/client.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/client.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -9,6 +9,7 @@
         const IS_IN_GAME = 0b0000_1000;
         const IS_JOINED_MID_GAME = 0b0001_0000;
         const IS_CHECKER = 0b0010_0000;
+        const IS_CONTRIBUTOR = 0b0100_0000;
 
         const NONE = 0b0000_0000;
         const DEFAULT = Self::NONE.bits;
@@ -66,6 +67,9 @@
     pub fn is_checker(&self) -> bool {
         self.contains(ClientFlags::IS_CHECKER)
     }
+    pub fn is_contributor(&self) -> bool {
+        self.contains(ClientFlags::IS_CONTRIBUTOR)
+    }
 
     pub fn set_is_admin(&mut self, value: bool) {
         self.set(ClientFlags::IS_ADMIN, value)
@@ -85,4 +89,7 @@
     pub fn set_is_checker(&mut self, value: bool) {
         self.set(ClientFlags::IS_CHECKER, value)
     }
+    pub fn set_is_contributor(&mut self, value: bool) {
+        self.set(ClientFlags::IS_CONTRIBUTOR, value)
+    }
 }
--- a/rust/hedgewars-server/src/server/core.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/core.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -2,13 +2,14 @@
     client::HWClient,
     coretypes::{ClientId, RoomId},
     indexslab::IndexSlab,
-    io::HWServerIO,
     room::HWRoom,
 };
 use crate::utils;
 
 use log::*;
 use slab;
+use std::fs::{File, OpenOptions};
+use std::io::{Read, Write};
 use std::{borrow::BorrowMut, iter, num::NonZeroU16};
 
 type Slab<T> = slab::Slab<T>;
@@ -16,7 +17,6 @@
 pub struct HWAnteClient {
     pub nick: Option<String>,
     pub protocol_number: Option<NonZeroU16>,
-    pub web_password: Option<String>,
     pub server_salt: String,
 }
 
@@ -35,20 +35,12 @@
             nick: None,
             protocol_number: None,
             server_salt: salt,
-            web_password: None,
         };
         self.clients.insert(client_id, client);
     }
 
     pub fn remove_client(&mut self, client_id: ClientId) -> Option<HWAnteClient> {
         let mut client = self.clients.remove(client_id);
-        if let Some(HWAnteClient {
-            web_password: Some(ref mut password),
-            ..
-        }) = client
-        {
-            password.replace_range(.., "🦔🦔🦔🦔🦔🦔🦔🦔");
-        }
         client
     }
 }
@@ -213,3 +205,48 @@
         }
     }
 }
+
+pub trait HWServerIO {
+    fn write_file(&mut self, name: &str, content: &str) -> std::io::Result<()>;
+    fn read_file(&mut self, name: &str) -> std::io::Result<String>;
+}
+
+pub struct EmptyServerIO {}
+
+impl EmptyServerIO {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl HWServerIO for EmptyServerIO {
+    fn write_file(&mut self, _name: &str, _content: &str) -> std::io::Result<()> {
+        Ok(())
+    }
+
+    fn read_file(&mut self, _name: &str) -> std::io::Result<String> {
+        Ok("".to_string())
+    }
+}
+
+pub struct FileServerIO {}
+
+impl FileServerIO {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl HWServerIO for FileServerIO {
+    fn write_file(&mut self, name: &str, content: &str) -> std::io::Result<()> {
+        let mut writer = OpenOptions::new().create(true).write(true).open(name)?;
+        writer.write_all(content.as_bytes())
+    }
+
+    fn read_file(&mut self, name: &str) -> std::io::Result<String> {
+        let mut reader = File::open(name)?;
+        let mut result = String::new();
+        reader.read_to_string(&mut result)?;
+        Ok(result)
+    }
+}
--- a/rust/hedgewars-server/src/server/database.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/database.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -1,11 +1,20 @@
 use mysql;
-use mysql::{error::DriverError, error::Error, params};
+use mysql::{error::DriverError, error::Error, from_row_opt, params};
+use openssl::sha::sha1;
+
+use super::handlers::AccountInfo;
+use crate::server::handlers::Sha1Digest;
 
-struct AccountInfo {
-    is_registered: bool,
-    is_admin: bool,
-    is_contributor: bool,
-}
+const GET_ACCOUNT_QUERY: &str =
+    r"SELECT CASE WHEN users.status = 1 THEN users.pass ELSE '' END,
+     (SELECT COUNT(users_roles.rid) FROM users_roles WHERE users.uid = users_roles.uid AND users_roles.rid = 3),
+     (SELECT COUNT(users_roles.rid) FROM users_roles WHERE users.uid = users_roles.uid AND users_roles.rid = 13)
+     FROM users WHERE users.name = :username";
+
+const STORE_STATS_QUERY: &str = r"INSERT INTO gameserver_stats
+            (players, rooms, last_update)
+            VALUES
+            (:players, :rooms, UNIX_TIMESTAMP())";
 
 struct ServerStatistics {
     rooms: u32,
@@ -14,47 +23,63 @@
 
 struct Achievements {}
 
-trait DatabaseInterface {
-    fn check_account(username: &str, password: &str) -> AccountInfo;
-    fn store_stats(stats: &ServerStatistics) -> Result<(), ()>;
-    fn store_achievements(achievements: &Achievements) -> Result<(), ()>;
-    fn get_replay_name(replay_id: u32) -> Result<String, ()>;
-}
-
-struct Database {
+pub struct Database {
     pool: Option<mysql::Pool>,
 }
 
 impl Database {
-    fn new() -> Self {
+    pub fn new() -> Self {
         Self { pool: None }
     }
 
-    fn connect(&mut self, url: &str) -> Result<(), Error> {
+    pub fn connect(&mut self, url: &str) -> Result<(), Error> {
         self.pool = Some(mysql::Pool::new(url)?);
 
         Ok(())
     }
 
-    fn check_account(&mut self, username: &str, password: &str) -> AccountInfo {
-        AccountInfo {
-            is_registered: false,
-            is_admin: false,
-            is_contributor: false,
+    pub fn get_account(
+        &mut self,
+        nick: &str,
+        protocol: u16,
+        password_hash: &str,
+        client_salt: &str,
+        server_salt: &str,
+    ) -> Result<Option<AccountInfo>, Error> {
+        if let Some(pool) = &self.pool {
+            if let Some(row) = pool.first_exec(GET_ACCOUNT_QUERY, params! { "username" => nick })? {
+                let (mut password, is_admin, is_contributor) =
+                    from_row_opt::<(String, i32, i32)>(row)?;
+                let client_hash = get_hash(protocol, &password, &client_salt, &server_salt);
+                let server_hash = get_hash(protocol, &password, &server_salt, &client_salt);
+                password.replace_range(.., "🦔🦔🦔🦔🦔🦔🦔🦔");
+
+                if server_hash == client_hash {
+                    Ok(Some(AccountInfo {
+                        is_registered: true,
+                        is_admin: is_admin == 1,
+                        is_contributor: is_contributor == 1,
+                        server_hash,
+                    }))
+                } else {
+                    Ok(None)
+                }
+            } else {
+                Ok(Some(AccountInfo {
+                    is_registered: false,
+                    is_admin: false,
+                    is_contributor: false,
+                    server_hash: Sha1Digest::new([0; 20]),
+                }))
+            }
+        } else {
+            Err(DriverError::SetupError.into())
         }
     }
 
-    fn store_stats(&mut self, stats: &ServerStatistics) -> Result<(), Error> {
+    pub fn store_stats(&mut self, stats: &ServerStatistics) -> Result<(), Error> {
         if let Some(pool) = &self.pool {
-            for mut stmt in pool
-                .prepare(
-                    r"INSERT INTO gameserver_stats
-            (players, rooms, last_update)
-            VALUES
-            (:players, :rooms, UNIX_TIMESTAMP())",
-                )
-                .into_iter()
-            {
+            for mut stmt in pool.prepare(STORE_STATS_QUERY).into_iter() {
                 stmt.execute(params! {
                     "players" => stats.players,
                     "rooms" => stats.rooms,
@@ -66,11 +91,19 @@
         }
     }
 
-    fn store_achievements(&mut self, achievements: &Achievements) -> Result<(), ()> {
+    pub fn store_achievements(&mut self, achievements: &Achievements) -> Result<(), ()> {
         Ok(())
     }
 
-    fn get_replay_name(&mut self, replay_id: u32) -> Result<String, ()> {
+    pub fn get_replay_name(&mut self, replay_id: u32) -> Result<String, ()> {
         Err(())
     }
 }
+
+fn get_hash(protocol_number: u16, web_password: &str, salt1: &str, salt2: &str) -> Sha1Digest {
+    let s = format!(
+        "{}{}{}{}{}",
+        salt1, salt2, web_password, protocol_number, "!hedgewars"
+    );
+    Sha1Digest::new(sha1(s.as_bytes()))
+}
--- a/rust/hedgewars-server/src/server/handlers.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/handlers.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -1,10 +1,11 @@
 use mio;
-use std::{io, io::Write};
+use std::{collections::HashMap, io, io::Write};
 
 use super::{
     actions::{Destination, DestinationRoom},
     core::HWServer,
     coretypes::ClientId,
+    room::RoomSave,
 };
 use crate::{
     protocol::messages::{HWProtocolMessage, HWServerMessage, HWServerMessage::*},
@@ -22,10 +23,51 @@
 mod loggingin;
 
 use self::loggingin::LoginResult;
+use std::fmt::{Formatter, LowerHex};
+
+#[derive(PartialEq)]
+pub struct Sha1Digest([u8; 20]);
+
+impl Sha1Digest {
+    pub fn new(digest: [u8; 20]) -> Self {
+        Self(digest)
+    }
+}
+
+impl LowerHex for Sha1Digest {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
+        for byte in &self.0 {
+            write!(f, "{:02x}", byte)?;
+        }
+        Ok(())
+    }
+}
+
+pub struct AccountInfo {
+    pub is_registered: bool,
+    pub is_admin: bool,
+    pub is_contributor: bool,
+    pub server_hash: Sha1Digest,
+}
+
+pub enum IoTask {
+    GetAccount {
+        nick: String,
+        protocol: u16,
+        password_hash: String,
+        client_salt: String,
+        server_salt: String,
+    },
+}
+
+pub enum IoResult {
+    Account(Option<AccountInfo>),
+}
 
 pub struct Response {
     client_id: ClientId,
     messages: Vec<PendingMessage>,
+    io_tasks: Vec<IoTask>,
     removed_clients: Vec<ClientId>,
 }
 
@@ -34,13 +76,14 @@
         Self {
             client_id,
             messages: vec![],
+            io_tasks: vec![],
             removed_clients: vec![],
         }
     }
 
     #[inline]
     pub fn is_empty(&self) -> bool {
-        self.messages.is_empty() && self.removed_clients.is_empty()
+        self.messages.is_empty() && self.removed_clients.is_empty() && self.io_tasks.is_empty()
     }
 
     #[inline]
@@ -58,6 +101,11 @@
         self.messages.push(message)
     }
 
+    #[inline]
+    pub fn request_io(&mut self, task: IoTask) {
+        self.io_tasks.push(task)
+    }
+
     pub fn extract_messages<'a, 'b: 'a>(
         &'b mut self,
         server: &'a HWServer,
@@ -76,6 +124,10 @@
     pub fn extract_removed_clients(&mut self) -> impl Iterator<Item = ClientId> + '_ {
         self.removed_clients.drain(..)
     }
+
+    pub fn extract_io_tasks(&mut self) -> impl Iterator<Item = IoTask> + '_ {
+        self.io_tasks.drain(..)
+    }
 }
 
 impl Extend<PendingMessage> for Response {
@@ -177,3 +229,26 @@
         common::remove_client(server, response, "Connection reset".to_string());
     }
 }
+
+pub fn handle_io_result(
+    server: &mut HWServer,
+    client_id: ClientId,
+    response: &mut Response,
+    io_result: IoResult,
+) {
+    match io_result {
+        IoResult::Account(Some(info)) => {
+            response.add(ServerAuth(format!("{:x}", info.server_hash)).send_self());
+            if let Some(client) = server.anteroom.remove_client(client_id) {
+                server.add_client(client_id, client);
+                let client = &mut server.clients[client_id];
+                client.set_is_admin(info.is_admin);
+                client.set_is_contributor(info.is_admin)
+            }
+        }
+        IoResult::Account(None) => {
+            response.add(Error("Authentication failed.".to_string()).send_self());
+            response.remove_client(client_id);
+        }
+    }
+}
--- a/rust/hedgewars-server/src/server/handlers/loggingin.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/handlers/loggingin.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -1,5 +1,6 @@
 use mio;
 
+use crate::protocol::messages::HWProtocolMessage::LoadRoom;
 use crate::{
     protocol::messages::{HWProtocolMessage, HWServerMessage::*},
     server::{
@@ -16,33 +17,25 @@
     num::NonZeroU16,
 };
 
-#[derive(PartialEq)]
-struct Sha1Digest([u8; 20]);
-
-impl LowerHex for Sha1Digest {
-    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
-        for byte in &self.0 {
-            write!(f, "{:02x}", byte)?;
-        }
-        Ok(())
-    }
-}
-
-#[cfg(feature = "official-server")]
-fn get_hash(protocol_number: u16, web_password: &str, salt1: &str, salt2: &str) -> Sha1Digest {
-    let s = format!(
-        "{}{}{}{}{}",
-        salt1, salt2, web_password, protocol_number, "!hedgewars"
-    );
-    Sha1Digest(sha1(s.as_bytes()))
-}
-
 pub enum LoginResult {
     Unchanged,
     Complete,
     Exit,
 }
 
+fn completion_result(client: &HWAnteClient, response: &mut super::Response) -> LoginResult {
+    #[cfg(feature = "official-server")]
+    {
+        response.add(AskPassword(client.server_salt.clone()).send_self());
+        LoginResult::Unchanged
+    }
+
+    #[cfg(not(feature = "official-server"))]
+    {
+        LoginResult::Complete
+    }
+}
+
 pub fn handle(
     anteroom: &mut HWAnteroom,
     client_id: ClientId,
@@ -68,7 +61,7 @@
                 response.add(Nick(nick).send_self());
 
                 if client.protocol_number.is_some() {
-                    LoginResult::Complete
+                    completion_result(&client, response)
                 } else {
                     LoginResult::Unchanged
                 }
@@ -87,7 +80,7 @@
                 response.add(Proto(proto).send_self());
 
                 if client.nick.is_some() {
-                    LoginResult::Complete
+                    completion_result(&client, response)
                 } else {
                     LoginResult::Unchanged
                 }
@@ -97,29 +90,23 @@
         HWProtocolMessage::Password(hash, salt) => {
             let client = &anteroom.clients[client_id];
 
-            if let (Some(protocol), Some(password)) =
-                (client.protocol_number, client.web_password.as_ref())
-            {
-                let client_hash = get_hash(protocol.get(), &password, &salt, &client.server_salt);
-                let server_hash = get_hash(protocol.get(), &password, &client.server_salt, &salt);
-                if client_hash == server_hash {
-                    response.add(ServerAuth(format!("{:x}", server_hash)).send_self());
-                    LoginResult::Complete
-                } else {
-                    response.add(Bye("No protocol provided.".to_string()).send_self());
-                    LoginResult::Unchanged
-                }
-            } else {
-                response.add(Bye("Authentication failed.".to_string()).send_self());
-                LoginResult::Exit
-            }
+            if let (Some(nick), Some(protocol)) = (client.nick.as_ref(), client.protocol_number) {
+                response.request_io(super::IoTask::GetAccount {
+                    nick: nick.clone(),
+                    protocol: protocol.get(),
+                    server_salt: client.server_salt.clone(),
+                    client_salt: salt,
+                    password_hash: hash,
+                });
+            };
+
+            LoginResult::Unchanged
         }
         #[cfg(feature = "official-server")]
         HWProtocolMessage::Checker(protocol, nick, password) => {
             let client = &mut anteroom.clients[client_id];
             client.protocol_number = NonZeroU16::new(protocol);
             client.nick = Some(nick);
-            client.web_password = Some(password);
             //client.set_is_checker(true);
             LoginResult::Complete
         }
--- a/rust/hedgewars-server/src/server/io.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/io.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -1,49 +1,73 @@
 use std::{
     fs::{File, OpenOptions},
     io::{Error, ErrorKind, Read, Result, Write},
+    sync::mpsc,
+    thread,
 };
 
-pub trait HWServerIO {
-    fn write_file(&mut self, name: &str, content: &str) -> Result<()>;
-    fn read_file(&mut self, name: &str) -> Result<String>;
+use crate::server::{
+    database::Database,
+    handlers::{IoResult, IoTask},
+};
+use mio::{Evented, Poll, PollOpt};
+use mio_extras::channel;
+
+pub type RequestId = u32;
+
+pub struct IOThread {
+    core_tx: mpsc::Sender<(RequestId, IoTask)>,
+    core_rx: channel::Receiver<(RequestId, IoResult)>,
 }
 
-pub struct EmptyServerIO {}
-
-impl EmptyServerIO {
+impl IOThread {
     pub fn new() -> Self {
-        Self {}
-    }
-}
+        let (core_tx, io_rx) = mpsc::channel();
+        let (io_tx, core_rx) = channel::channel();
+
+        let mut db = Database::new();
+        db.connect("localhost");
 
-impl HWServerIO for EmptyServerIO {
-    fn write_file(&mut self, _name: &str, _content: &str) -> Result<()> {
-        Ok(())
+        thread::spawn(move || {
+            while let Ok((request_id, task)) = io_rx.try_recv() {
+                match task {
+                    IoTask::GetAccount {
+                        nick,
+                        protocol,
+                        password_hash,
+                        client_salt,
+                        server_salt,
+                    } => {
+                        if let Ok(account) = db.get_account(
+                            &nick,
+                            protocol,
+                            &password_hash,
+                            &client_salt,
+                            &server_salt,
+                        ) {
+                            io_tx.send((request_id, IoResult::Account(account)));
+                        }
+                    }
+                }
+            }
+        });
+
+        Self { core_rx, core_tx }
     }
 
-    fn read_file(&mut self, _name: &str) -> Result<String> {
-        Ok("".to_string())
+    pub fn send(&self, request_id: RequestId, task: IoTask) {
+        self.core_tx.send((request_id, task)).unwrap();
     }
-}
 
-pub struct FileServerIO {}
+    pub fn try_recv(&self) -> Option<(RequestId, IoResult)> {
+        match self.core_rx.try_recv() {
+            Ok(result) => Some(result),
+            Err(mpsc::TryRecvError::Empty) => None,
+            Err(mpsc::TryRecvError::Disconnected) => unreachable!(),
+        }
+    }
 
-impl FileServerIO {
-    pub fn new() -> Self {
-        Self {}
+    pub fn register_rx(&self, poll: &mio::Poll, token: mio::Token) -> Result<()> {
+        self.core_rx
+            .register(poll, token, mio::Ready::readable(), PollOpt::edge())
     }
 }
-
-impl HWServerIO for FileServerIO {
-    fn write_file(&mut self, name: &str, content: &str) -> Result<()> {
-        let mut writer = OpenOptions::new().create(true).write(true).open(name)?;
-        writer.write_all(content.as_bytes())
-    }
-
-    fn read_file(&mut self, name: &str) -> Result<String> {
-        let mut reader = File::open(name)?;
-        let mut result = String::new();
-        reader.read_to_string(&mut result)?;
-        Ok(result)
-    }
-}
--- a/rust/hedgewars-server/src/server/network.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/server/network.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -16,11 +16,16 @@
 use netbuf;
 use slab::Slab;
 
-use super::{core::HWServer, coretypes::ClientId, handlers, io::FileServerIO};
+use super::{core::FileServerIO, core::HWServer, coretypes::ClientId, handlers};
 use crate::{
     protocol::{messages::*, ProtocolDecoder},
     utils,
 };
+
+#[cfg(feature = "official-server")]
+use super::io::{IOThread, RequestId};
+
+use crate::server::handlers::{IoResult, IoTask};
 #[cfg(feature = "tls-connections")]
 use openssl::{
     error::ErrorStack,
@@ -234,6 +239,56 @@
     context: SslContext,
 }
 
+#[cfg(feature = "official-server")]
+pub struct IoLayer {
+    next_request_id: RequestId,
+    request_queue: Vec<(RequestId, ClientId)>,
+    io_thread: IOThread,
+}
+
+#[cfg(feature = "official-server")]
+impl IoLayer {
+    fn new() -> Self {
+        Self {
+            next_request_id: 0,
+            request_queue: vec![],
+            io_thread: IOThread::new(),
+        }
+    }
+
+    fn send(&mut self, client_id: ClientId, task: IoTask) {
+        let request_id = self.next_request_id;
+        self.next_request_id += 1;
+        self.request_queue.push((request_id, client_id));
+        self.io_thread.send(request_id, task);
+    }
+
+    fn try_recv(&mut self) -> Option<(ClientId, IoResult)> {
+        let (request_id, result) = self.io_thread.try_recv()?;
+        if let Some(index) = self
+            .request_queue
+            .iter()
+            .position(|(id, _)| *id == request_id)
+        {
+            let (_, client_id) = self.request_queue.swap_remove(index);
+            Some((client_id, result))
+        } else {
+            None
+        }
+    }
+
+    fn cancel(&mut self, client_id: ClientId) {
+        let mut index = 0;
+        while index < self.request_queue.len() {
+            if self.request_queue[index].1 == client_id {
+                self.request_queue.swap_remove(index);
+            } else {
+                index += 1;
+            }
+        }
+    }
+}
+
 pub struct NetworkLayer {
     listener: TcpListener,
     server: HWServer,
@@ -242,6 +297,8 @@
     pending_cache: Vec<(ClientId, NetworkClientState)>,
     #[cfg(feature = "tls-connections")]
     ssl: ServerSsl,
+    #[cfg(feature = "official-server")]
+    io: IoLayer,
 }
 
 impl NetworkLayer {
@@ -259,6 +316,8 @@
             pending_cache,
             #[cfg(feature = "tls-connections")]
             ssl: NetworkLayer::create_ssl_context(),
+            #[cfg(feature = "official-server")]
+            io: IoLayer::new(),
         }
     }
 
@@ -283,10 +342,15 @@
     pub fn register_server(&self, poll: &Poll) -> io::Result<()> {
         poll.register(
             &self.listener,
-            utils::SERVER,
+            utils::SERVER_TOKEN,
             Ready::readable(),
             PollOpt::edge(),
-        )
+        )?;
+
+        #[cfg(feature = "official-server")]
+        self.io.io_thread.register_rx(poll, utils::IO_TOKEN)?;
+
+        Ok(())
     }
 
     fn deregister_client(&mut self, poll: &Poll, id: ClientId) {
@@ -299,6 +363,8 @@
         }
         if client_exists {
             self.clients.remove(id);
+            #[cfg(feature = "official-server")]
+            self.io.cancel(id);
         }
     }
 
@@ -326,7 +392,7 @@
         client_id
     }
 
-    fn flush_server_messages(&mut self, mut response: handlers::Response, poll: &Poll) {
+    fn handle_response(&mut self, mut response: handlers::Response, poll: &Poll) {
         debug!("{} pending server messages", response.len());
         let output = response.extract_messages(&mut self.server);
         for (clients, message) in output {
@@ -344,6 +410,22 @@
         for client_id in response.extract_removed_clients() {
             self.deregister_client(poll, client_id);
         }
+
+        #[cfg(feature = "official-server")]
+        {
+            let client_id = response.client_id();
+            for task in response.extract_io_tasks() {
+                self.io.send(client_id, task);
+            }
+        }
+    }
+
+    #[cfg(feature = "official-server")]
+    pub fn handle_io_result(&mut self) {
+        if let Some((client_id, result)) = self.io.try_recv() {
+            let mut response = handlers::Response::new(client_id);
+            handlers::handle_io_result(&mut self.server, client_id, &mut response, result);
+        }
     }
 
     fn create_client_socket(&self, socket: TcpStream) -> io::Result<ClientSocket> {
@@ -381,7 +463,7 @@
         handlers::handle_client_accept(&mut self.server, client_id, &mut response);
 
         if !response.is_empty() {
-            self.flush_server_messages(response, poll);
+            self.handle_response(response, poll);
         }
 
         Ok(())
@@ -438,7 +520,7 @@
         }
 
         if !response.is_empty() {
-            self.flush_server_messages(response, poll);
+            self.handle_response(response, poll);
         }
 
         Ok(())
@@ -469,7 +551,7 @@
         self.deregister_client(poll, client_id);
         let mut response = handlers::Response::new(client_id);
         handlers::handle_client_loss(&mut self.server, client_id, &mut response);
-        self.flush_server_messages(response, poll);
+        self.handle_response(response, poll);
 
         Ok(())
     }
--- a/rust/hedgewars-server/src/utils.rs	Tue Apr 09 00:45:14 2019 +0200
+++ b/rust/hedgewars-server/src/utils.rs	Tue Apr 09 21:08:35 2019 +0300
@@ -3,7 +3,8 @@
 use std::iter::Iterator;
 
 pub const PROTOCOL_VERSION: u32 = 3;
-pub const SERVER: mio::Token = mio::Token(1_000_000_000);
+pub const SERVER_TOKEN: mio::Token = mio::Token(1_000_000_000);
+pub const IO_TOKEN: mio::Token = mio::Token(1_000_000_001);
 
 pub fn is_name_illegal(name: &str) -> bool {
     name.len() > 40