diff --git a/src/bin/kawari-login.rs b/src/bin/kawari-login.rs index dc1177a..6690095 100644 --- a/src/bin/kawari-login.rs +++ b/src/bin/kawari-login.rs @@ -1,107 +1,16 @@ use std::net::SocketAddr; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use axum::extract::{Query, State}; use axum::response::{Html, Redirect}; use axum::routing::post; use axum::{Form, Router, routing::get}; -use rand::Rng; -use rand::distributions::Alphanumeric; -use rusqlite::Connection; +use kawari::login::{LoginDatabase, LoginError}; use serde::Deserialize; -fn generate_sid() -> String { - let random_id: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(56) - .map(char::from) - .collect(); - random_id.to_lowercase() -} - -pub enum LoginError { - WrongUsername, - WrongPassword, - InternalError, -} - #[derive(Clone)] struct LoginServerState { - connection: Arc>, -} - -impl LoginServerState { - /// Adds a new user to the database. - fn add_user(&self, username: &str, password: &str) { - let connection = self.connection.lock().unwrap(); - - tracing::info!("Adding user with username {username}"); - - let query = "INSERT INTO users VALUES (?1, ?2);"; - connection - .execute(query, (username, password)) - .expect("Failed to write user to database!"); - } - - /// Login as user, returns a session id. - fn login_user(&self, username: &str, password: &str) -> Result { - let selected_row: Result<(String, String), rusqlite::Error>; - - tracing::info!("Finding user with username {username}"); - - { - let connection = self.connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT username, password FROM users WHERE username = ?1") - .map_err(|_err| LoginError::WrongUsername)?; - selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?))); - } - - if let Ok((_user, their_password)) = selected_row { - if their_password == password { - return self - .create_session(username) - .ok_or(LoginError::InternalError); - } else { - return Err(LoginError::WrongPassword); - } - } - - Err(LoginError::WrongUsername) - } - - /// Create a new session for user, which replaces the last one (if any) - fn create_session(&self, username: &str) -> Option { - let connection = self.connection.lock().unwrap(); - - let sid = generate_sid(); - - connection - .execute( - "INSERT OR REPLACE INTO sessions VALUES (?1, ?2);", - (username, &sid), - ) - .ok()?; - - tracing::info!("Created new session for {username}: {sid}"); - - Some(sid) - } - - /// Checks if there is a valid session for a given id - fn check_session(&self, sid: &str) -> bool { - let connection = self.connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT username, sid FROM sessions WHERE sid = ?1") - .ok() - .unwrap(); - let selected_row: Result<(String, String), rusqlite::Error> = - stmt.query_row((sid,), |row| Ok((row.get(0)?, row.get(1)?))); - - selected_row.is_ok() - } + database: Arc, } #[derive(Deserialize)] @@ -134,7 +43,7 @@ async fn login_send( State(state): State, Form(input): Form, ) -> Html { - let user = state.login_user(&input.sqexid, &input.password); + let user = state.database.login_user(&input.sqexid, &input.password); match user { Ok(session_id) => Html(format!( "window.external.user(\"login=auth,ok,sid,{session_id},terms,1,region,2,etmadd,0,playable,1,ps3pkg,0,maxex,4,product,1\");" @@ -181,7 +90,7 @@ async fn do_register( panic!("Expected password!"); }; - state.add_user(&username, &password); + state.database.add_user(&username, &password); Redirect::to("/") } @@ -196,38 +105,20 @@ async fn check_session( State(state): State, Query(params): Query, ) -> String { - if state.check_session(¶ms.sid) { + if state.database.check_session(¶ms.sid) { "1".to_string() } else { "0".to_string() } } -fn setup_state() -> LoginServerState { - let connection = Connection::open("login.db").expect("Failed to open database!"); - - // Create users table - { - let query = "CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);"; - connection.execute(query, ()).unwrap(); - } - - // Create active sessions table - { - let query = "CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);"; - connection.execute(query, ()).unwrap(); - } - - LoginServerState { - connection: Arc::new(Mutex::new(connection)), - } -} - #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - let state = setup_state(); + let state = LoginServerState { + database: Arc::new(LoginDatabase::new()), + }; let app = Router::new() .route("/oauth/ffxivarr/login/top", get(top)) diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index c642279..1c77492 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,16 +1,12 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; -use kawari::WORLD_NAME; use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; use kawari::config::get_config; -use kawari::lobby::ipc::CharacterDetails; -use kawari::lobby::{CharaMake, ClientSelectData}; use kawari::oodle::OodleNetwork; use kawari::packet::{ CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive, send_packet, }; -use kawari::world::PlayerData; use kawari::world::ipc::{ ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData, ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect, @@ -22,215 +18,13 @@ use kawari::world::{ Position, SocialList, }, }; +use kawari::world::{PlayerData, WorldDatabase}; use kawari::{CHAR_NAME, CITY_STATE, CONTENT_ID, WORLD_ID, ZONE_ID, common::timestamp_secs}; use physis::common::{Language, Platform}; use physis::gamedata::GameData; -use rusqlite::Connection; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; -fn setup_db() -> Arc> { - let connection = Connection::open("world.db").expect("Failed to open database!"); - - // Create characters table - { - let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);"; - connection.execute(query, ()).unwrap(); - } - - // Create characters data table - { - let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);"; - connection.execute(query, ()).unwrap(); - } - - Arc::new(Mutex::new(connection)) -} - -fn find_player_data(connection: &Arc>, actor_id: u32) -> PlayerData { - let connection = connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1") - .unwrap(); - let (content_id, account_id) = stmt - .query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?))) - .unwrap(); - - PlayerData { - actor_id, - content_id, - account_id, - } -} - -// TODO: from/to sql int - -fn find_actor_id(connection: &Arc>, content_id: u64) -> u32 { - let connection = connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT actor_id FROM characters WHERE content_id = ?1") - .unwrap(); - - stmt.query_row((content_id,), |row| row.get(0)).unwrap() -} - -fn get_character_list( - connection: &Arc>, - service_account_id: u32, -) -> Vec { - let connection = connection.lock().unwrap(); - - let content_actor_ids: Vec<(u32, u32)>; - - // find the content ids associated with the service account - { - let mut stmt = connection - .prepare("SELECT content_id, actor_id FROM characters WHERE service_account_id = ?1") - .unwrap(); - - content_actor_ids = stmt - .query_map((service_account_id,), |row| Ok((row.get(0)?, row.get(1)?))) - .unwrap() - .map(|x| x.unwrap()) - .collect(); - } - - let mut characters = Vec::new(); - - for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() { - dbg!(content_id); - - let mut stmt = connection - .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") - .unwrap(); - - let (name, chara_make): (String, String) = stmt - .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) - .unwrap(); - - let chara_make = CharaMake::from_json(&chara_make); - - let select_data = ClientSelectData { - game_name_unk: "Final Fantasy".to_string(), - current_class: 2, - class_levels: [5; 30], - race: chara_make.customize.race as i32, - subrace: chara_make.customize.subrace as i32, - gender: chara_make.customize.gender as i32, - birth_month: chara_make.birth_month, - birth_day: chara_make.birth_day, - guardian: chara_make.guardian, - unk8: 0, - unk9: 0, - zone_id: ZONE_ID as i32, - unk11: 0, - customize: chara_make.customize, - unk12: 0, - unk13: 0, - unk14: [0; 10], - unk15: 0, - unk16: 0, - legacy_character: 0, - unk18: 0, - unk19: 0, - unk20: 0, - unk21: String::new(), - unk22: 0, - unk23: 0, - }; - - characters.push(CharacterDetails { - actor_id: *actor_id, - content_id: *content_id as u64, - index: index as u32, - unk1: [0; 16], - origin_server_id: WORLD_ID, - current_server_id: WORLD_ID, - character_name: name.clone(), - origin_server_name: WORLD_NAME.to_string(), - current_server_name: WORLD_NAME.to_string(), - character_detail_json: select_data.to_json(), - unk2: [0; 20], - }); - } - - dbg!(&characters); - - characters -} - -fn generate_content_id() -> u32 { - rand::random() -} - -fn generate_actor_id() -> u32 { - rand::random() -} - -/// Gives (content_id, actor_id) -fn create_player_data( - connection: &Arc>, - name: &str, - chara_make: &str, -) -> (u64, u32) { - let content_id = generate_content_id(); - let actor_id = generate_actor_id(); - - let connection = connection.lock().unwrap(); - - // insert ids - connection - .execute( - "INSERT INTO characters VALUES (?1, ?2, ?3);", - (content_id, 0x1, actor_id), - ) - .unwrap(); - - // insert char data - connection - .execute( - "INSERT INTO character_data VALUES (?1, ?2, ?3);", - (content_id, name, chara_make), - ) - .unwrap(); - - (content_id as u64, actor_id) -} - -/// Checks if `name` is in the character data table -fn check_is_name_free(connection: &Arc>, name: &str) -> bool { - let connection = connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT content_id FROM character_data WHERE name = ?1") - .unwrap(); - - !stmt.exists((name,)).unwrap() -} - -struct CharacterData { - name: String, - chara_make: CharaMake, // probably not the ideal way to store this? -} - -fn find_chara_make(connection: &Arc>, content_id: u64) -> CharacterData { - let connection = connection.lock().unwrap(); - - let mut stmt = connection - .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") - .unwrap(); - let (name, chara_make_json): (String, String) = stmt - .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) - .unwrap(); - - CharacterData { - name, - chara_make: CharaMake::from_json(&chara_make_json), - } -} - #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -239,12 +33,12 @@ async fn main() { tracing::info!("World server started on 127.0.0.1:7100"); - let db_connection = setup_db(); + let database = Arc::new(WorldDatabase::new()); loop { let (socket, _) = listener.accept().await.unwrap(); - let db_connection = db_connection.clone(); + let database = database.clone(); let state = PacketState { client_key: None, @@ -279,10 +73,8 @@ async fn main() { tracing::info!("actor id to parse: {actor_id}"); // collect actor data - connection.player_data = find_player_data( - &db_connection, - actor_id.parse::().unwrap(), - ); + connection.player_data = + database.find_player_data(actor_id.parse::().unwrap()); println!("player data: {:#?}", connection.player_data); @@ -442,10 +234,8 @@ async fn main() { // Player Setup { - let chara_details = find_chara_make( - &db_connection, - connection.player_data.content_id, - ); + let chara_details = database + .find_chara_make(connection.player_data.content_id); let ipc = ServerZoneIpcSegment { op_code: ServerZoneIpcType::PlayerSetup, @@ -515,10 +305,8 @@ async fn main() { "Client has finished loading... spawning in!" ); - let chara_details = find_chara_make( - &db_connection, - connection.player_data.content_id, - ); + let chara_details = database + .find_chara_make(connection.player_data.content_id); // send player spawn { @@ -925,11 +713,8 @@ async fn main() { "creating character from: {name} {chara_make_json}" ); - let (content_id, actor_id) = create_player_data( - &db_connection, - name, - chara_make_json, - ); + let (content_id, actor_id) = + database.create_player_data(name, chara_make_json); tracing::info!( "Created new player: {content_id} {actor_id}" @@ -960,7 +745,7 @@ async fn main() { } } CustomIpcData::GetActorId { content_id } => { - let actor_id = find_actor_id(&db_connection, *content_id); + let actor_id = database.find_actor_id(*content_id); tracing::info!("We found an actor id: {actor_id}"); @@ -987,7 +772,7 @@ async fn main() { } } CustomIpcData::CheckNameIsAvailable { name } => { - let is_name_free = check_is_name_free(&db_connection, name); + let is_name_free = database.check_is_name_free(name); let is_name_free = if is_name_free { 1 } else { 0 }; // send response @@ -1014,7 +799,7 @@ async fn main() { } CustomIpcData::RequestCharacterList { service_account_id } => { let characters = - get_character_list(&db_connection, *service_account_id); + database.get_character_list(*service_account_id); // send response { diff --git a/src/lib.rs b/src/lib.rs index a29f55d..c507928 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,9 @@ pub mod world; /// Everything packet parsing related. pub mod packet; +/// Logic server-specific code. +pub mod login; + // TODO: make this configurable /// The world ID and name for the lobby. /// See for a list of possible IDs. diff --git a/src/login/database.rs b/src/login/database.rs new file mode 100644 index 0000000..f11ebd3 --- /dev/null +++ b/src/login/database.rs @@ -0,0 +1,120 @@ +use std::sync::Mutex; + +use rand::Rng; +use rand::distributions::Alphanumeric; +use rusqlite::Connection; + +pub struct LoginDatabase { + connection: Mutex, +} + +pub enum LoginError { + WrongUsername, + WrongPassword, + InternalError, +} + +impl LoginDatabase { + pub fn new() -> Self { + let connection = Connection::open("login.db").expect("Failed to open database!"); + + // Create users table + { + let query = + "CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);"; + connection.execute(query, ()).unwrap(); + } + + // Create active sessions table + { + let query = + "CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);"; + connection.execute(query, ()).unwrap(); + } + + Self { + connection: Mutex::new(connection), + } + } + + /// Adds a new user to the database. + pub fn add_user(&self, username: &str, password: &str) { + let connection = self.connection.lock().unwrap(); + + tracing::info!("Adding user with username {username}"); + + let query = "INSERT INTO users VALUES (?1, ?2);"; + connection + .execute(query, (username, password)) + .expect("Failed to write user to database!"); + } + + /// Login as user, returns a session id. + pub fn login_user(&self, username: &str, password: &str) -> Result { + let selected_row: Result<(String, String), rusqlite::Error>; + + tracing::info!("Finding user with username {username}"); + + { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT username, password FROM users WHERE username = ?1") + .map_err(|_err| LoginError::WrongUsername)?; + selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?))); + } + + if let Ok((_user, their_password)) = selected_row { + if their_password == password { + return self + .create_session(username) + .ok_or(LoginError::InternalError); + } else { + return Err(LoginError::WrongPassword); + } + } + + Err(LoginError::WrongUsername) + } + + fn generate_sid() -> String { + let random_id: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(56) + .map(char::from) + .collect(); + random_id.to_lowercase() + } + + /// Create a new session for user, which replaces the last one (if any) + pub fn create_session(&self, username: &str) -> Option { + let connection = self.connection.lock().unwrap(); + + let sid = Self::generate_sid(); + + connection + .execute( + "INSERT OR REPLACE INTO sessions VALUES (?1, ?2);", + (username, &sid), + ) + .ok()?; + + tracing::info!("Created new session for {username}: {sid}"); + + Some(sid) + } + + /// Checks if there is a valid session for a given id + pub fn check_session(&self, sid: &str) -> bool { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT username, sid FROM sessions WHERE sid = ?1") + .ok() + .unwrap(); + let selected_row: Result<(String, String), rusqlite::Error> = + stmt.query_row((sid,), |row| Ok((row.get(0)?, row.get(1)?))); + + selected_row.is_ok() + } +} diff --git a/src/login/mod.rs b/src/login/mod.rs new file mode 100644 index 0000000..171fcba --- /dev/null +++ b/src/login/mod.rs @@ -0,0 +1,2 @@ +mod database; +pub use database::{LoginDatabase, LoginError}; diff --git a/src/world/database.rs b/src/world/database.rs new file mode 100644 index 0000000..2ae01df --- /dev/null +++ b/src/world/database.rs @@ -0,0 +1,213 @@ +use std::sync::Mutex; + +use rusqlite::Connection; + +use crate::{ + WORLD_ID, WORLD_NAME, ZONE_ID, + lobby::{CharaMake, ClientSelectData, ipc::CharacterDetails}, +}; + +use super::PlayerData; + +pub struct WorldDatabase { + connection: Mutex, +} + +pub struct CharacterData { + pub name: String, + pub chara_make: CharaMake, // probably not the ideal way to store this? +} + +impl WorldDatabase { + pub fn new() -> Self { + let connection = Connection::open("world.db").expect("Failed to open database!"); + + // Create characters table + { + let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);"; + connection.execute(query, ()).unwrap(); + } + + // Create characters data table + { + let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);"; + connection.execute(query, ()).unwrap(); + } + + Self { + connection: Mutex::new(connection), + } + } + + pub fn find_player_data(&self, actor_id: u32) -> PlayerData { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1") + .unwrap(); + let (content_id, account_id) = stmt + .query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + PlayerData { + actor_id, + content_id, + account_id, + } + } + + // TODO: from/to sql int + + pub fn find_actor_id(&self, content_id: u64) -> u32 { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT actor_id FROM characters WHERE content_id = ?1") + .unwrap(); + + stmt.query_row((content_id,), |row| row.get(0)).unwrap() + } + + pub fn get_character_list(&self, service_account_id: u32) -> Vec { + let connection = self.connection.lock().unwrap(); + + let content_actor_ids: Vec<(u32, u32)>; + + // find the content ids associated with the service account + { + let mut stmt = connection + .prepare( + "SELECT content_id, actor_id FROM characters WHERE service_account_id = ?1", + ) + .unwrap(); + + content_actor_ids = stmt + .query_map((service_account_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|x| x.unwrap()) + .collect(); + } + + let mut characters = Vec::new(); + + for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() { + dbg!(content_id); + + let mut stmt = connection + .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") + .unwrap(); + + let (name, chara_make): (String, String) = stmt + .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + let chara_make = CharaMake::from_json(&chara_make); + + let select_data = ClientSelectData { + game_name_unk: "Final Fantasy".to_string(), + current_class: 2, + class_levels: [5; 30], + race: chara_make.customize.race as i32, + subrace: chara_make.customize.subrace as i32, + gender: chara_make.customize.gender as i32, + birth_month: chara_make.birth_month, + birth_day: chara_make.birth_day, + guardian: chara_make.guardian, + unk8: 0, + unk9: 0, + zone_id: ZONE_ID as i32, + unk11: 0, + customize: chara_make.customize, + unk12: 0, + unk13: 0, + unk14: [0; 10], + unk15: 0, + unk16: 0, + legacy_character: 0, + unk18: 0, + unk19: 0, + unk20: 0, + unk21: String::new(), + unk22: 0, + unk23: 0, + }; + + characters.push(CharacterDetails { + actor_id: *actor_id, + content_id: *content_id as u64, + index: index as u32, + unk1: [0; 16], + origin_server_id: WORLD_ID, + current_server_id: WORLD_ID, + character_name: name.clone(), + origin_server_name: WORLD_NAME.to_string(), + current_server_name: WORLD_NAME.to_string(), + character_detail_json: select_data.to_json(), + unk2: [0; 20], + }); + } + + characters + } + + fn generate_content_id() -> u32 { + rand::random() + } + + fn generate_actor_id() -> u32 { + rand::random() + } + + /// Gives (content_id, actor_id) + pub fn create_player_data(&self, name: &str, chara_make: &str) -> (u64, u32) { + let content_id = Self::generate_content_id(); + let actor_id = Self::generate_actor_id(); + + let connection = self.connection.lock().unwrap(); + + // insert ids + connection + .execute( + "INSERT INTO characters VALUES (?1, ?2, ?3);", + (content_id, 0x1, actor_id), + ) + .unwrap(); + + // insert char data + connection + .execute( + "INSERT INTO character_data VALUES (?1, ?2, ?3);", + (content_id, name, chara_make), + ) + .unwrap(); + + (content_id as u64, actor_id) + } + + /// Checks if `name` is in the character data table + pub fn check_is_name_free(&self, name: &str) -> bool { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT content_id FROM character_data WHERE name = ?1") + .unwrap(); + + !stmt.exists((name,)).unwrap() + } + + pub fn find_chara_make(&self, content_id: u64) -> CharacterData { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") + .unwrap(); + let (name, chara_make_json): (String, String) = stmt + .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + CharacterData { + name, + chara_make: CharaMake::from_json(&chara_make_json), + } + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs index 8056f71..9a40076 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -8,3 +8,6 @@ pub use chat_handler::ChatHandler; mod connection; pub use connection::{PlayerData, ZoneConnection}; + +mod database; +pub use database::{CharacterData, WorldDatabase};