mirror of
https://github.com/redstrate/Kawari.git
synced 2025-05-02 03:07:44 +00:00
245 lines
9.8 KiB
Rust
245 lines
9.8 KiB
Rust
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 axum_extra::extract::CookieJar;
|
|
use axum_extra::extract::cookie::{Cookie, Expiration};
|
|
use kawari::config::get_config;
|
|
use kawari::login::{LoginDatabase, LoginError};
|
|
use minijinja::{Environment, context};
|
|
use serde::Deserialize;
|
|
|
|
fn setup_default_environment() -> Environment<'static> {
|
|
let mut env = Environment::new();
|
|
env.add_template("login.html", include_str!("../../templates/login.html"))
|
|
.unwrap();
|
|
env.add_template(
|
|
"register.html",
|
|
include_str!("../../templates/register.html"),
|
|
)
|
|
.unwrap();
|
|
env.add_template("account.html", include_str!("../../templates/account.html"))
|
|
.unwrap();
|
|
env.add_template(
|
|
"changepassword.html",
|
|
include_str!("../../templates/changepassword.html"),
|
|
)
|
|
.unwrap();
|
|
|
|
env
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct LoginServerState {
|
|
database: Arc<LoginDatabase>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct Params {
|
|
lng: String,
|
|
rgn: String,
|
|
isft: String,
|
|
cssmode: String,
|
|
isnew: String,
|
|
launchver: String,
|
|
}
|
|
|
|
async fn top(Query(_): Query<Params>) -> Html<&'static str> {
|
|
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>",
|
|
)
|
|
}
|
|
|
|
#[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<LoginServerState>,
|
|
Form(input): Form<Input>,
|
|
) -> Html<String> {
|
|
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,5,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<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.database.add_user(&username, &password);
|
|
|
|
Redirect::to("/")
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct CheckSessionParams {
|
|
sid: String,
|
|
}
|
|
|
|
async fn check_session(
|
|
State(state): State<LoginServerState>,
|
|
Query(params): Query<CheckSessionParams>,
|
|
) -> String {
|
|
let accounts = state.database.check_session(¶ms.sid);
|
|
serde_json::to_string(&accounts).unwrap_or(String::new())
|
|
}
|
|
|
|
async fn login() -> Html<String> {
|
|
let environment = setup_default_environment();
|
|
let template = environment.get_template("login.html").unwrap();
|
|
Html(template.render(context! {}).unwrap())
|
|
}
|
|
|
|
async fn register() -> Html<String> {
|
|
let environment = setup_default_environment();
|
|
let template = environment.get_template("register.html").unwrap();
|
|
Html(template.render(context! {}).unwrap())
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[allow(dead_code)]
|
|
struct LoginInput {
|
|
username: Option<String>,
|
|
password: Option<String>,
|
|
}
|
|
|
|
async fn do_login(
|
|
State(state): State<LoginServerState>,
|
|
jar: CookieJar,
|
|
Form(input): Form<LoginInput>,
|
|
) -> (CookieJar, Redirect) {
|
|
tracing::info!("{:#?} logging in!", input.username,);
|
|
|
|
let Some(username) = input.username else {
|
|
panic!("Expected username!");
|
|
};
|
|
let Some(password) = input.password else {
|
|
panic!("Expected password!");
|
|
};
|
|
|
|
let sid = state.database.login_user(&username, &password).unwrap();
|
|
|
|
let cookie = Cookie::build(("cis_sessid", sid))
|
|
.path("/")
|
|
.secure(false)
|
|
.expires(Expiration::Session)
|
|
.http_only(true);
|
|
|
|
(jar.add(cookie), Redirect::to("/account/app/svc/manage"))
|
|
}
|
|
|
|
async fn account(State(state): State<LoginServerState>, jar: CookieJar) -> Html<String> {
|
|
if let Some(session_id) = jar.get("cis_sessid") {
|
|
let user_id = state.database.get_user_id(session_id.value());
|
|
let username = state.database.get_username(user_id);
|
|
|
|
let environment = setup_default_environment();
|
|
let template = environment.get_template("account.html").unwrap();
|
|
Html(template.render(context! { username => username }).unwrap())
|
|
} else {
|
|
Html("You need to be logged in!".to_string())
|
|
}
|
|
}
|
|
|
|
async fn logout(jar: CookieJar) -> (CookieJar, Redirect) {
|
|
let config = get_config();
|
|
// TODO: remove session from database
|
|
(
|
|
jar.remove("cis_sessid"),
|
|
Redirect::to(&format!("http://{}/", config.web.server_name)),
|
|
)
|
|
}
|
|
|
|
async fn change_password() -> Html<String> {
|
|
// TODO: actually change password
|
|
let environment = setup_default_environment();
|
|
let template = environment.get_template("changepassword.html").unwrap();
|
|
Html(template.render(context! {}).unwrap())
|
|
}
|
|
|
|
async fn cancel_account(jar: CookieJar) -> (CookieJar, Redirect) {
|
|
// TODO: actually delete account
|
|
(jar.remove("cis_sessid"), Redirect::to("/"))
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
let state = LoginServerState {
|
|
database: Arc::new(LoginDatabase::new()),
|
|
};
|
|
|
|
let app = Router::new()
|
|
// retail API
|
|
.route("/oauth/ffxivarr/login/top", get(top))
|
|
.route("/oauth/ffxivarr/login/login.send", post(login_send))
|
|
// private server<->server API
|
|
// TODO: make these actually private
|
|
.route("/_private/service_accounts", get(check_session))
|
|
// public website
|
|
.route("/oauth/oa/oauthlogin", get(login))
|
|
.route("/oauth/oa/oauthlogin", post(do_login))
|
|
.route("/oauth/oa/registligt", get(register))
|
|
.route("/oauth/oa/registlist", post(do_register))
|
|
.route("/account/app/svc/manage", get(account))
|
|
.route("/account/app/svc/logout", get(logout))
|
|
.route("/account/app/svc/mbrPasswd", get(change_password))
|
|
.route("/account/app/svc/mbrCancel", get(cancel_account))
|
|
.with_state(state);
|
|
|
|
let config = get_config();
|
|
|
|
let addr = config.login.get_socketaddr();
|
|
tracing::info!("Server started on {addr}");
|
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
axum::serve(listener, app).await.unwrap();
|
|
}
|