use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use axum::extract::{Query, State}; use axum::response::{Html, Redirect}; use axum::routing::post; use axum::{Form, Router, routing::get}; use kawari::generate_sid; use rusqlite::Connection; use serde::Deserialize; 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(); 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>; { 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() } } #[derive(Deserialize)] #[allow(dead_code)] struct Params { lng: String, rgn: String, isft: String, cssmode: String, isnew: String, launchver: String, } async fn top(Query(_): Query) -> Html<&'static str> { Html( "\r\n\r\n\r\n\r\n
\r\n\t\r\n\t\t\r\n\t\t\n\r\n\t\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\r\n\t\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\t\t\r\n\r\n\t\t
\r\n\t\t\t\r\n\t\t
\r\n\r\n\t
\r\n\r\n\r\n\r\n\r\n", ) } #[derive(Deserialize, Debug)] #[allow(dead_code, non_snake_case)] struct Input { _STORED_: String, sqexid: String, password: String, otppw: String, } async fn login_send( State(state): State, Form(input): Form, ) -> Html { let user = state.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\");" )), Err(err) => { // TODO: see what the official error messages are match err { LoginError::WrongUsername => Html("window.external.user(\"login=auth,ng,err,Wrong Username\");".to_string()), LoginError::WrongPassword => Html("window.external.user(\"login=auth,ng,err,Wrong Password\");".to_string()), LoginError::InternalError => Html("window.external.user(\"login=auth,ng,err,Internal Server Error\");".to_string()), } } } } #[derive(Deserialize, Debug)] #[allow(dead_code)] struct RegisterInput { username: Option, password: Option, } async fn do_register( State(state): State, Form(input): Form, ) -> Redirect { tracing::info!( "Registering with {:#?} and {:#?}!", input.username, input.password ); let Some(username) = input.username else { panic!("Expected username!"); }; let Some(password) = input.password else { panic!("Expected password!"); }; state.add_user(&username, &password); Redirect::to("/") } #[derive(Deserialize)] #[allow(dead_code)] struct CheckSessionParams { sid: String, } async fn check_session( State(state): State, Query(params): Query, ) -> String { if state.check_session(¶ms.sid) { "1".to_string() } else { "0".to_string() } } fn setup_state() -> LoginServerState { let connection = Connection::open_in_memory().expect("Failed to open database!"); // Create users table { let query = "CREATE TABLE users (username TEXT PRIMARY KEY, password TEXT);"; connection.execute(query, ()).unwrap(); } // Create active sessions table { let query = "CREATE TABLE 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 app = Router::new() .route("/oauth/ffxivarr/login/top", get(top)) .route("/oauth/ffxivarr/login/login.send", post(login_send)) .route("/register", post(do_register)) // TODO: make these actually private .route("/private/check_session", get(check_session)) .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], 6700)); tracing::info!("Login server started on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }