1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-22 23:27:46 +00:00

Integrate Login<->Lobby servers, remove placeholder service account id

All accounts were sharing the same character list, but now they should
be properly separated. This also modifies the login database to prepare
for multiple service accounts, but there's no way to manage them in the
web interface yet still.
This commit is contained in:
Joshua Goins 2025-04-05 21:36:56 -04:00
parent 19b84f4164
commit 90e5e191e9
10 changed files with 1083 additions and 63 deletions

957
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -90,3 +90,6 @@ mlua = { version = "0.10", features = ["lua51", "vendored", "send", "async"], de
# For character backup decompression # For character backup decompression
zip = { version = "2.5", features = ["deflate", "lzma", "bzip2"], default-features = false } zip = { version = "2.5", features = ["deflate", "lzma", "bzip2"], default-features = false }
# For some login<->lobby server communication
reqwest = "0.12.15"

View file

@ -5,6 +5,7 @@ use kawari::common::custom_ipc::CustomIpcSegment;
use kawari::common::custom_ipc::CustomIpcType; use kawari::common::custom_ipc::CustomIpcType;
use kawari::config::get_config; use kawari::config::get_config;
use kawari::lobby::LobbyConnection; use kawari::lobby::LobbyConnection;
use kawari::lobby::ipc::ServiceAccount;
use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment}; use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment};
use kawari::lobby::send_custom_world_packet; use kawari::lobby::send_custom_world_packet;
use kawari::oodle::OodleNetwork; use kawari::oodle::OodleNetwork;
@ -43,6 +44,8 @@ async fn main() {
session_id: None, session_id: None,
stored_character_creation_name: String::new(), stored_character_creation_name: String::new(),
world_name: world_name.clone(), world_name: world_name.clone(),
service_accounts: Vec::new(),
selected_service_account: None,
}; };
tokio::spawn(async move { tokio::spawn(async move {
@ -63,22 +66,42 @@ async fn main() {
} }
SegmentType::Ipc { data } => match &data.data { SegmentType::Ipc { data } => match &data.data {
ClientLobbyIpcData::ClientVersionInfo { ClientLobbyIpcData::ClientVersionInfo {
sequence,
session_id, session_id,
version_info, version_info,
..
} => { } => {
tracing::info!( tracing::info!(
"Client {session_id} ({version_info}) logging in!" "Client {session_id} ({version_info}) logging in!"
); );
let config = get_config();
let body = reqwest::get(format!(
"http://{}/_private/service_accounts?sid={}",
config.login.get_socketaddr(),
session_id
))
.await
.unwrap()
.text()
.await
.unwrap();
let service_accounts: Option<Vec<ServiceAccount>> =
serde_json::from_str(&body).ok();
if let Some(service_accounts) = service_accounts {
connection.service_accounts = service_accounts;
connection.session_id = Some(session_id.clone()); connection.session_id = Some(session_id.clone());
connection.send_account_list().await; connection.send_account_list().await;
} else {
// request an update // request an update, wrong error message lol
//connection.send_error(*sequence, 1012, 13101).await; connection.send_error(*sequence, 1012, 13101).await;
}
} }
ClientLobbyIpcData::RequestCharacterList { sequence } => { ClientLobbyIpcData::RequestCharacterList { sequence } => {
// TODO: support selecting a service account
connection.selected_service_account =
Some(connection.service_accounts[0].id);
connection.send_lobby_info(*sequence).await connection.send_lobby_info(*sequence).await
} }
ClientLobbyIpcData::LobbyCharacterAction(character_action) => { ClientLobbyIpcData::LobbyCharacterAction(character_action) => {

View file

@ -105,11 +105,8 @@ async fn check_session(
State(state): State<LoginServerState>, State(state): State<LoginServerState>,
Query(params): Query<CheckSessionParams>, Query(params): Query<CheckSessionParams>,
) -> String { ) -> String {
if state.database.check_session(&params.sid) { let accounts = state.database.check_session(&params.sid);
"1".to_string() serde_json::to_string(&accounts).unwrap_or(String::new())
} else {
"0".to_string()
}
} }
#[tokio::main] #[tokio::main]
@ -125,7 +122,7 @@ async fn main() {
.route("/oauth/ffxivarr/login/login.send", post(login_send)) .route("/oauth/ffxivarr/login/login.send", post(login_send))
.route("/register", post(do_register)) .route("/register", post(do_register))
// TODO: make these actually private // TODO: make these actually private
.route("/private/check_session", get(check_session)) .route("/_private/service_accounts", get(check_session))
.with_state(state); .with_state(state);
let config = get_config(); let config = get_config();

View file

@ -805,6 +805,7 @@ async fn client_loop(
SegmentType::CustomIpc { data } => { SegmentType::CustomIpc { data } => {
match &data.data { match &data.data {
CustomIpcData::RequestCreateCharacter { CustomIpcData::RequestCreateCharacter {
service_account_id,
name, name,
chara_make_json, chara_make_json,
} => { } => {
@ -829,6 +830,7 @@ async fn client_loop(
); );
let (content_id, actor_id) = database.create_player_data( let (content_id, actor_id) = database.create_player_data(
*service_account_id,
name, name,
chara_make_json, chara_make_json,
city_state, city_state,

View file

@ -61,6 +61,7 @@ pub enum CustomIpcType {
pub enum CustomIpcData { pub enum CustomIpcData {
#[br(pre_assert(*magic == CustomIpcType::RequestCreateCharacter))] #[br(pre_assert(*magic == CustomIpcType::RequestCreateCharacter))]
RequestCreateCharacter { RequestCreateCharacter {
service_account_id: u32,
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)] #[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)] #[br(map = read_string)]
@ -107,6 +108,7 @@ pub enum CustomIpcData {
impl Default for CustomIpcData { impl Default for CustomIpcData {
fn default() -> CustomIpcData { fn default() -> CustomIpcData {
CustomIpcData::RequestCreateCharacter { CustomIpcData::RequestCreateCharacter {
service_account_id: 0,
chara_make_json: String::new(), chara_make_json: String::new(),
name: String::new(), name: String::new(),
} }

View file

@ -37,6 +37,10 @@ pub struct LobbyConnection {
pub stored_character_creation_name: String, pub stored_character_creation_name: String,
pub world_name: String, pub world_name: String,
pub service_accounts: Vec<ServiceAccount>,
pub selected_service_account: Option<u32>,
} }
impl LobbyConnection { impl LobbyConnection {
@ -79,22 +83,13 @@ impl LobbyConnection {
/// Send the service account list to the client. /// Send the service account list to the client.
pub async fn send_account_list(&mut self) { pub async fn send_account_list(&mut self) {
// send the client the service account list
let service_accounts = [ServiceAccount {
id: 0x002E4A2B,
unk1: 0,
index: 0,
name: "FINAL FANTASY XIV".to_string(),
}]
.to_vec();
let service_account_list = let service_account_list =
ServerLobbyIpcData::LobbyServiceAccountList(LobbyServiceAccountList { ServerLobbyIpcData::LobbyServiceAccountList(LobbyServiceAccountList {
sequence: 0, sequence: 0,
num_service_accounts: service_accounts.len() as u8, num_service_accounts: self.service_accounts.len() as u8,
unk1: 3, unk1: 3,
unk2: 0x99, unk2: 0x99,
service_accounts: service_accounts.to_vec(), service_accounts: self.service_accounts.to_vec(),
}); });
let ipc = ServerLobbyIpcSegment { let ipc = ServerLobbyIpcSegment {
@ -196,7 +191,7 @@ impl LobbyConnection {
server_id: 0, server_id: 0,
timestamp: 0, timestamp: 0,
data: CustomIpcData::RequestCharacterList { data: CustomIpcData::RequestCharacterList {
service_account_id: 0x1, // TODO: placeholder service_account_id: self.selected_service_account.unwrap(),
}, },
}; };
@ -441,6 +436,7 @@ impl LobbyConnection {
server_id: 0, server_id: 0,
timestamp: 0, timestamp: 0,
data: CustomIpcData::RequestCreateCharacter { data: CustomIpcData::RequestCreateCharacter {
service_account_id: self.selected_service_account.unwrap(),
name: self.stored_character_creation_name.clone(), // TODO: worth double-checking, but AFAIK we have to store it this way? name: self.stored_character_creation_name.clone(), // TODO: worth double-checking, but AFAIK we have to store it this way?
chara_make_json: character_action.json.clone(), chara_make_json: character_action.json.clone(),
}, },

View file

@ -1,9 +1,10 @@
use binrw::binrw; use binrw::binrw;
use serde::{Deserialize, Serialize};
use crate::common::{read_string, write_string}; use crate::common::{read_string, write_string};
#[binrw] #[binrw]
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ServiceAccount { pub struct ServiceAccount {
pub id: u32, pub id: u32,
pub unk1: u32, pub unk1: u32,

View file

@ -4,6 +4,8 @@ use rand::Rng;
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use rusqlite::Connection; use rusqlite::Connection;
use crate::lobby::ipc::ServiceAccount;
pub struct LoginDatabase { pub struct LoginDatabase {
connection: Mutex<Connection>, connection: Mutex<Connection>,
} }
@ -26,15 +28,20 @@ impl LoginDatabase {
// Create users table // Create users table
{ {
let query = let query = "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT);";
"CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);";
connection.execute(query, ()).unwrap(); connection.execute(query, ()).unwrap();
} }
// Create active sessions table // Create active sessions table
{ {
let query = let query =
"CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);"; "CREATE TABLE IF NOT EXISTS sessions (user_id INTEGER PRIMARY KEY, sid TEXT);";
connection.execute(query, ()).unwrap();
}
// Create service accounts table
{
let query = "CREATE TABLE IF NOT EXISTS service_accounts (id INTEGER PRIMARY KEY, user_id INTEGER);";
connection.execute(query, ()).unwrap(); connection.execute(query, ()).unwrap();
} }
@ -43,21 +50,45 @@ impl LoginDatabase {
} }
} }
fn generate_account_id() -> u32 {
rand::random()
}
/// Adds a new user to the database. /// Adds a new user to the database.
pub fn add_user(&self, username: &str, password: &str) { pub fn add_user(&self, username: &str, password: &str) {
if self.check_username(username) {
tracing::info!("{username} already taken!");
return;
}
let user_id = Self::generate_account_id();
// add user
{
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
tracing::info!("Adding user with username {username}"); tracing::info!("Adding user with username {username}");
let query = "INSERT INTO users VALUES (?1, ?2);"; let query = "INSERT INTO users VALUES (?1, ?2, ?3);";
connection connection
.execute(query, (username, password)) .execute(query, (user_id, username, password))
.expect("Failed to write user to database!"); .expect("Failed to write user to database!");
} }
// add service account
{
let connection = self.connection.lock().unwrap();
let query = "INSERT INTO service_accounts VALUES (?1, ?2);";
connection
.execute(query, (Self::generate_account_id(), user_id))
.expect("Failed to write service account to database!");
}
}
/// Login as user, returns a session id. /// Login as user, returns a session id.
pub fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> { pub fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> {
let selected_row: Result<(String, String), rusqlite::Error>; let selected_row: Result<(u32, String), rusqlite::Error>;
tracing::info!("Finding user with username {username}"); tracing::info!("Finding user with username {username}");
@ -65,16 +96,14 @@ impl LoginDatabase {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let mut stmt = connection let mut stmt = connection
.prepare("SELECT username, password FROM users WHERE username = ?1") .prepare("SELECT id, password FROM users WHERE username = ?1")
.map_err(|_err| LoginError::WrongUsername)?; .map_err(|_err| LoginError::WrongUsername)?;
selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?))); selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?)));
} }
if let Ok((_user, their_password)) = selected_row { if let Ok((_id, their_password)) = selected_row {
if their_password == password { if their_password == password {
return self return self.create_session(_id).ok_or(LoginError::InternalError);
.create_session(username)
.ok_or(LoginError::InternalError);
} else { } else {
return Err(LoginError::WrongPassword); return Err(LoginError::WrongPassword);
} }
@ -93,7 +122,7 @@ impl LoginDatabase {
} }
/// Create a new session for user, which replaces the last one (if any) /// Create a new session for user, which replaces the last one (if any)
pub fn create_session(&self, username: &str) -> Option<String> { pub fn create_session(&self, user_id: u32) -> Option<String> {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let sid = Self::generate_sid(); let sid = Self::generate_sid();
@ -101,25 +130,63 @@ impl LoginDatabase {
connection connection
.execute( .execute(
"INSERT OR REPLACE INTO sessions VALUES (?1, ?2);", "INSERT OR REPLACE INTO sessions VALUES (?1, ?2);",
(username, &sid), (user_id, &sid),
) )
.ok()?; .ok()?;
tracing::info!("Created new session for {username}: {sid}"); tracing::info!("Created new session for account {user_id}: {sid}");
Some(sid) Some(sid)
} }
/// Checks if there is a valid session for a given id /// Gets the service account list
pub fn check_session(&self, sid: &str) -> bool { pub fn check_session(&self, sid: &str) -> Vec<ServiceAccount> {
let connection = self.connection.lock().unwrap();
// get user id
let user_id: u32;
{
let mut stmt = connection
.prepare("SELECT user_id FROM sessions WHERE sid = ?1")
.ok()
.unwrap();
user_id = stmt.query_row((sid,), |row| Ok(row.get(0)?)).unwrap();
}
// service accounts
{
let mut stmt = connection
.prepare("SELECT id FROM service_accounts WHERE user_id = ?1")
.ok()
.unwrap();
let accounts = stmt.query_map((user_id,), |row| row.get(0)).unwrap();
let mut service_accounts = Vec::new();
let mut index = 0;
for id in accounts {
service_accounts.push(ServiceAccount {
id: id.unwrap(),
unk1: 0,
index,
name: format!("FINAL FANTASY XIV {}", index + 1), // TODO: don't add the "1" if you only have one service account
});
index += 1
}
service_accounts
}
}
/// Checks if a username is taken
pub fn check_username(&self, username: &str) -> bool {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let mut stmt = connection let mut stmt = connection
.prepare("SELECT username, sid FROM sessions WHERE sid = ?1") .prepare("SELECT id FROM users WHERE username = ?1")
.ok() .ok()
.unwrap(); .unwrap();
let selected_row: Result<(String, String), rusqlite::Error> = let selected_row: Result<u32, rusqlite::Error> =
stmt.query_row((sid,), |row| Ok((row.get(0)?, row.get(1)?))); stmt.query_row((username,), |row| Ok(row.get(0)?));
selected_row.is_ok() selected_row.is_ok()
} }

View file

@ -127,6 +127,7 @@ impl WorldDatabase {
// TODO: import inventory // TODO: import inventory
self.create_player_data( self.create_player_data(
0x1,
&character.name, &character.name,
&chara_make.to_json(), &chara_make.to_json(),
character.city_state.value as u8, character.city_state.value as u8,
@ -322,6 +323,7 @@ impl WorldDatabase {
/// Gives (content_id, actor_id) /// Gives (content_id, actor_id)
pub fn create_player_data( pub fn create_player_data(
&self, &self,
service_account_id: u32,
name: &str, name: &str,
chara_make: &str, chara_make: &str,
city_state: u8, city_state: u8,
@ -337,7 +339,7 @@ impl WorldDatabase {
connection connection
.execute( .execute(
"INSERT INTO characters VALUES (?1, ?2, ?3);", "INSERT INTO characters VALUES (?1, ?2, ?3);",
(content_id, 0x1, actor_id), (content_id, service_account_id, actor_id),
) )
.unwrap(); .unwrap();