2023-10-05 13:09:21 -04:00
|
|
|
use std::net::SocketAddr;
|
2025-03-16 20:55:51 -04:00
|
|
|
use std::sync::{Arc, Mutex};
|
2023-10-05 13:09:21 -04:00
|
|
|
|
2025-03-16 20:55:51 -04:00
|
|
|
use axum::extract::{Query, State};
|
|
|
|
use axum::response::{Html, Redirect};
|
2023-10-05 13:09:21 -04:00
|
|
|
use axum::routing::post;
|
2025-03-08 13:27:41 -05:00
|
|
|
use axum::{Form, Router, routing::get};
|
2025-03-18 19:55:49 -04:00
|
|
|
use rand::Rng;
|
|
|
|
use rand::distributions::Alphanumeric;
|
2025-03-16 20:55:51 -04:00
|
|
|
use rusqlite::Connection;
|
2024-05-11 12:32:46 -04:00
|
|
|
use serde::Deserialize;
|
2023-10-05 13:09:21 -04:00
|
|
|
|
2025-03-18 19:55:49 -04:00
|
|
|
fn generate_sid() -> String {
|
|
|
|
let random_id: String = rand::thread_rng()
|
|
|
|
.sample_iter(&Alphanumeric)
|
|
|
|
.take(56)
|
|
|
|
.map(char::from)
|
|
|
|
.collect();
|
|
|
|
random_id.to_lowercase()
|
|
|
|
}
|
|
|
|
|
2025-03-16 20:55:51 -04:00
|
|
|
pub enum LoginError {
|
|
|
|
WrongUsername,
|
|
|
|
WrongPassword,
|
2025-03-16 21:15:58 -04:00
|
|
|
InternalError,
|
2025-03-16 20:55:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct LoginServerState {
|
|
|
|
connection: Arc<Mutex<Connection>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
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<String, LoginError> {
|
2025-03-16 21:15:58 -04:00
|
|
|
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)?)));
|
|
|
|
}
|
2025-03-16 20:55:51 -04:00
|
|
|
|
|
|
|
if let Ok((_user, their_password)) = selected_row {
|
|
|
|
if their_password == password {
|
2025-03-16 21:15:58 -04:00
|
|
|
return self
|
|
|
|
.create_session(username)
|
2025-03-17 17:31:22 -04:00
|
|
|
.ok_or(LoginError::InternalError);
|
2025-03-16 20:55:51 -04:00
|
|
|
} else {
|
|
|
|
return Err(LoginError::WrongPassword);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Err(LoginError::WrongUsername)
|
|
|
|
}
|
2025-03-16 21:15:58 -04:00
|
|
|
|
|
|
|
/// Create a new session for user, which replaces the last one (if any)
|
|
|
|
fn create_session(&self, username: &str) -> Option<String> {
|
|
|
|
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()
|
|
|
|
}
|
2025-03-16 20:55:51 -04:00
|
|
|
}
|
|
|
|
|
2023-10-05 13:32:08 -04:00
|
|
|
#[derive(Deserialize)]
|
2024-05-11 12:32:46 -04:00
|
|
|
#[allow(dead_code)]
|
2023-10-05 13:32:08 -04:00
|
|
|
struct Params {
|
|
|
|
lng: String,
|
|
|
|
rgn: String,
|
|
|
|
isft: String,
|
|
|
|
cssmode: String,
|
|
|
|
isnew: String,
|
2025-03-08 13:27:41 -05:00
|
|
|
launchver: String,
|
2023-10-05 13:32:08 -04:00
|
|
|
}
|
|
|
|
|
2025-03-08 23:07:55 -05:00
|
|
|
async fn top(Query(_): Query<Params>) -> Html<&'static str> {
|
2025-03-08 13:27:41 -05:00
|
|
|
Html(
|
|
|
|
"\r\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\r\n<html lang=en-GB id=gb>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head><form action=\"login.send\" method=\"post\" name=\"mainForm\">\r\n\t\r\n\t\t\r\n\t\t<input type=\"hidden\" name=\"_STORED_\" value=\"42f06e5f4194001a9ad61c8481f435e8b9eac79242f9221d463aa492ab2b3373655adadff3e72dd16a798ee8a222c519848743c97084f1af71854f06050a1f2813e5c3aaf66e5f0ef24dc18588a8cf06758992e42035f7e4f99f85c8b6082200dcabc6a37c7f76ce542eefc1f1798da5e23fd4b46ed17489de5eb8e8a222c5198487433bff5f3433c061ded661b3f33b5f2d2807f5db74747f4dfe8f1fe89f9388f717347bbea9e9ec2931bb6fdc4b11648dfa9e726cdf690d74970a36f7482c12593a5ad7b966c4cf14655e11641f0bb67b8e807377edfa81055480da52031e0ba86ec52f991eb3cb8913c8f807287f3cb5ac4143326f33a4503cf31e021c8f41a5eec01870e0004acc0d0bf2bed65da5eeae3703ae878c20bd7f1167745e96770979146463fa40235e6bba8bdac1273dcbc1256cda0caacbdaad\">\n\r\n\t\t\r\n\t\t<div class=\"form-item type-id\">\r\n\t\t\t<label class=\"item-label\" for=\"sqexid\"><span class=\"label-image-text\" title=\"Square Enix ID\"></span></label>\r\n\t\t\t<input class=\"item-input\" name=\"sqexid\" id=\"sqexid\" type=\"text\" value=\"\" tabindex=\"1\" placeholder=\"ID (Required)\" maxLength=\"16\"\r\n\t\t\t\r\n\t\t\t\t />\r\n\t\t\t\r\n\t\t</div>\r\n\r\n\t\t <div class=\"form-item type-pw\">\r\n\t\t\t<label class=\"item-label\" for=\"password\"><span class=\"label-image-text\" title=\"Square Enix Password\"></span></label>\r\n\t\t\t<input class=\"item-password\" name=\"password\" id=\"password\" type=\"password\" value=\"\" tabindex=\"2\" placeholder=\"Password (Required)\" maxLength=\"32\" autocomplete=\"off\"/>\r\n\t\t</div>\r\n\t\r\n\t\t<div class=\"form-item type-otpw\">\r\n\t\t\t<label class=\"item-label\" for=\"otppw\"><span class=\"label-image-text\" title=\"One-Time Password\"></span></label>\r\n\t\t\t<input class=\"item-otpw\" name=\"otppw\" id=\"otppw\" type=\"text\" value=\"\" tabindex=\"3\" autocomplete=\"off\" maxLength=\"6\" placeholder=\"Password (Optional)\" />\r\n\t\t</div>\r\n\r\n\t\t\r\n\t\t<div class=\"form-item type-remember-id\">\r\n\t\t\t<input name=\"saveid\" id=\"saveid\" type=\"checkbox\" value=\"1\" class=\"item-checkbox\" tabindex=\"4\" />\r\n\t\t\t<label class=\"item-checkbox-label\" for=\"saveid\"><span class=\"label-checkbox-image-text\" title=\"Remember Square Enix ID\"></span></label>\r\n\t\t</div>\r\n\t\t\r\n\r\n\t\t<div class=\"form-item type-submit\">\r\n\t\t\t<button class=\"item-button\" type=\"submit\" tabindex=\"5\" onClick=\"ctrEvent('mainForm')\" id=\"btLogin\"><span class=\"button-image-text\" title=\"Login\"></span></button>\r\n\t\t</div>\r\n\r\n\t</form>\r\n</div>\r\n</body>\r\n</html>\r\n\r\n</html>",
|
|
|
|
)
|
2023-10-05 13:09:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
2024-05-11 12:32:46 -04:00
|
|
|
#[allow(dead_code, non_snake_case)]
|
2023-10-05 13:09:21 -04:00
|
|
|
struct Input {
|
|
|
|
_STORED_: String,
|
|
|
|
sqexid: String,
|
|
|
|
password: String,
|
2025-03-08 13:27:41 -05:00
|
|
|
otppw: String,
|
2023-10-05 13:09:21 -04:00
|
|
|
}
|
|
|
|
|
2025-03-16 20:55:51 -04:00
|
|
|
async fn login_send(
|
|
|
|
State(state): State<LoginServerState>,
|
|
|
|
Form(input): Form<Input>,
|
|
|
|
) -> Html<String> {
|
|
|
|
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 {
|
2025-03-17 17:36:43 -04:00
|
|
|
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(),
|
|
|
|
),
|
2025-03-16 20:55:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(dead_code)]
|
|
|
|
struct RegisterInput {
|
|
|
|
username: Option<String>,
|
|
|
|
password: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn do_register(
|
|
|
|
State(state): State<LoginServerState>,
|
|
|
|
Form(input): Form<RegisterInput>,
|
|
|
|
) -> 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("/")
|
|
|
|
}
|
|
|
|
|
2025-03-16 21:15:58 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(dead_code)]
|
|
|
|
struct CheckSessionParams {
|
|
|
|
sid: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn check_session(
|
|
|
|
State(state): State<LoginServerState>,
|
|
|
|
Query(params): Query<CheckSessionParams>,
|
|
|
|
) -> String {
|
|
|
|
if state.check_session(¶ms.sid) {
|
2025-03-17 17:31:22 -04:00
|
|
|
"1".to_string()
|
2025-03-16 21:15:58 -04:00
|
|
|
} else {
|
2025-03-17 17:31:22 -04:00
|
|
|
"0".to_string()
|
2025-03-16 21:15:58 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-16 20:55:51 -04:00
|
|
|
fn setup_state() -> LoginServerState {
|
|
|
|
let connection = Connection::open_in_memory().expect("Failed to open database!");
|
|
|
|
|
2025-03-16 21:15:58 -04:00
|
|
|
// 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();
|
|
|
|
}
|
2025-03-16 20:55:51 -04:00
|
|
|
|
|
|
|
LoginServerState {
|
|
|
|
connection: Arc::new(Mutex::new(connection)),
|
|
|
|
}
|
2023-10-05 13:09:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() {
|
|
|
|
tracing_subscriber::fmt::init();
|
|
|
|
|
2025-03-16 20:55:51 -04:00
|
|
|
let state = setup_state();
|
|
|
|
|
2023-10-05 13:09:21 -04:00
|
|
|
let app = Router::new()
|
2023-10-05 13:24:00 -04:00
|
|
|
.route("/oauth/ffxivarr/login/top", get(top))
|
2025-03-16 20:55:51 -04:00
|
|
|
.route("/oauth/ffxivarr/login/login.send", post(login_send))
|
|
|
|
.route("/register", post(do_register))
|
2025-03-16 21:15:58 -04:00
|
|
|
// TODO: make these actually private
|
|
|
|
.route("/private/check_session", get(check_session))
|
2025-03-16 20:55:51 -04:00
|
|
|
.with_state(state);
|
2023-10-05 13:09:21 -04:00
|
|
|
|
|
|
|
let addr = SocketAddr::from(([127, 0, 0, 1], 6700));
|
2024-05-11 12:21:35 -04:00
|
|
|
tracing::info!("Login server started on {}", addr);
|
2023-10-05 13:09:21 -04:00
|
|
|
axum::Server::bind(&addr)
|
|
|
|
.serve(app.into_make_service())
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2025-03-08 13:27:41 -05:00
|
|
|
}
|