1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-19 22:36:49 +00:00

Add a basic login system

This allows you to register in the web frontend now, and the login server now
checks this before giving a session ID. Note that this is wildly insecure (it
stores the passwords in plaintext!) and is duly noted in the USAGE.
This commit is contained in:
Joshua Goins 2025-03-16 20:55:51 -04:00
parent 3140dad378
commit bd23c04848
6 changed files with 224 additions and 35 deletions

105
Cargo.lock generated
View file

@ -42,7 +42,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-util",
"headers",
@ -134,6 +134,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -155,6 +161,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -202,12 +217,30 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -287,6 +320,24 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown",
]
[[package]]
name = "headers"
version = "0.3.9"
@ -384,6 +435,7 @@ dependencies = [
"minijinja",
"physis",
"rand",
"rusqlite",
"serde",
"serde_json",
"tokio",
@ -403,6 +455,17 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libsqlite3-sys"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-rs-sys"
version = "0.4.2"
@ -492,7 +555,7 @@ version = "0.4.0"
source = "git+https://github.com/redstrate/physis#d3918c13824f5cab6c7c41255437b69c4fb17ee2"
dependencies = [
"binrw",
"bitflags",
"bitflags 1.3.2",
"half",
"libz-rs-sys",
"tracing",
@ -530,6 +593,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -587,6 +656,20 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rusqlite"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
dependencies = [
"bitflags 2.9.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -679,6 +762,18 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "socket2"
version = "0.5.8"
@ -829,6 +924,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"

View file

@ -65,3 +65,6 @@ md5 = "0.7.0"
# Used to access game data
physis = { git = "https://github.com/redstrate/physis" }
# Used for data persistence
rusqlite = { version = "0.34.0", features = ["bundled"] }

View file

@ -1,7 +1,9 @@
# USage
# Usage
Kawari is designed to be easy to run, with the goal of being accessible to anyone who wants to run a local server for themselves.
**Note:** Persisted data (logins, characters, etc) is _not_ stable or secure. Treat all data as disposable.
## Copyright Notice
**Kawari requires that you have an active subscription**, and are in possession of a legitimate copy of the game. Kawari is not related or affiliated to SqEx, and by using it you are in explicit violation of the User Agreement (_Limitation 2.4_.)

View file

@ -1,12 +1,56 @@
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use axum::extract::Query;
use axum::response::Html;
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,
}
#[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> {
let connection = self.connection.lock().unwrap();
let mut stmt = connection
.prepare("SELECT username, password FROM users WHERE username = ?1")
.map_err(|_err| LoginError::WrongUsername)?;
let selected_row: Result<(String, String), rusqlite::Error> =
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 Ok(generate_sid());
} else {
return Err(LoginError::WrongPassword);
}
}
Err(LoginError::WrongUsername)
}
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct Params {
@ -33,20 +77,80 @@ struct Input {
otppw: String,
}
async fn login_send(Form(_): Form<Input>) -> Html<String> {
let sid = generate_sid();
Html(format!(
"window.external.user(\"login=auth,ok,sid,{sid},terms,1,region,2,etmadd,0,playable,1,ps3pkg,0,maxex,4,product,1\");"
))
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 {
LoginError::WrongUsername => Html(format!(
"window.external.user(\"login=auth,ng,err,Wrong Username\");"
)),
LoginError::WrongPassword => Html(format!(
"window.external.user(\"login=auth,ng,err,Wrong Password\");"
)),
}
}
}
}
#[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("/")
}
fn setup_state() -> LoginServerState {
let connection = Connection::open_in_memory().expect("Failed to open database!");
let query = "CREATE TABLE users (username TEXT, password 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("/oauth/ffxivarr/login/login.send", post(login_send))
.route("/register", post(do_register))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 6700));
tracing::info!("Login server started on {}", addr);

View file

@ -43,26 +43,6 @@ async fn world_status() -> Html<String> {
)
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Input {
login_open: Option<String>,
worlds_open: Option<String>,
}
async fn apply(Form(input): Form<Input>) -> Redirect {
tracing::info!("Apply config changes...");
let mut config = get_config();
config.login_open = input.login_open == Some("1".to_string());
config.worlds_open = input.worlds_open == Some("1".to_string());
serde_json::to_writer(&std::fs::File::create("config.json").unwrap(), &config)
.expect("TODO: panic message");
Redirect::to("/")
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
@ -71,8 +51,7 @@ async fn main() {
.route("/", get(root))
.route("/login", get(login))
.route("/register", get(register))
.route("/worldstatus", get(world_status))
.route("/apply", post(apply));
.route("/worldstatus", get(world_status));
let addr = SocketAddr::from(([127, 0, 0, 1], 5801));
tracing::info!("Web server started on {}", addr);

View file

@ -6,7 +6,7 @@
</head>
<body>
<form method='post'>
<form action='http://ffxiv-login.square.local/register' method='post'>
<label for="username">Username:</label><br>
<input type='text' id='username' name='username'/><br>
<label for="password">Password:</label><br>
@ -15,4 +15,4 @@
</form>
</body>
</html>
</html>