diff --git a/Cargo.lock b/Cargo.lock index 45f4f38..d1bb30c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1495,6 +1495,7 @@ dependencies = [ "serde", "serde_json", "thiserror 1.0.69", + "walkdir", ] [[package]] @@ -2595,7 +2596,6 @@ dependencies = [ "jsonwebtoken", "libfj", "log", - "oj_rc_core", "rocket", "rocket_dyn_templates", "serde", @@ -3346,6 +3346,25 @@ dependencies = [ "zerocopy 0.8.14", ] +[[package]] +name = "rc_auth" +version = "0.3.0" +dependencies = [ + "actix-files", + "actix-web", + "clap", + "env_logger", + "git-version", + "handlebars", + "hex", + "libfj", + "log", + "oj_rc_core", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "rc_multiplayer" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index b5173bb..f033f2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ readme = "README.md" [workspace] members = [ "auth", "cdn", + "rc_auth", "polariton_auth", "rc_services", "rc_services_room", "rc_microtransactions", diff --git a/auth/Cargo.toml b/auth/Cargo.toml index 2a2384a..750e6d8 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -9,9 +9,8 @@ authors.workspace = true [features] steam = ["steamworks"] -robocraft = ["oj_rc_core"] cardlife = [] -default = ["robocraft", "cardlife"] +default = ["cardlife"] [dependencies] rocket.workspace = true @@ -24,6 +23,5 @@ steamworks = { version = "0.11", optional = true } hex = "0.4" jsonwebtoken = "9" clap.workspace = true -oj_rc_core = { version = "*", optional = true, path = "../rc_core" } serde.workspace = true git-version.workspace = true diff --git a/auth/src/common/cli.rs b/auth/src/common/cli.rs index 31a01f3..2d6f3d4 100644 --- a/auth/src/common/cli.rs +++ b/auth/src/common/cli.rs @@ -3,10 +3,6 @@ use clap::Parser; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct CliArgs { - #[cfg(feature = "robocraft")] - /// Robocraft user data root - #[arg(long, default_value_t = {"../data/robocraft".to_string()})] - pub data_robocraft: String, /// Robocraft asset data root #[arg(long, default_value_t = {"../assets/robocraft".to_string()})] pub assets_robocraft: String, @@ -19,13 +15,9 @@ impl CliArgs { pub async fn preloaded(self) -> Config { Config { - #[cfg(feature = "robocraft")] - robocraft: crate::robocraft::RcConfig::from_args(&self).await, } } } pub struct Config { - #[cfg(feature = "robocraft")] - pub robocraft: crate::robocraft::RcConfig, } diff --git a/auth/src/common/steam_utils.rs b/auth/src/common/steam_utils.rs index f1ff031..bea8079 100644 --- a/auth/src/common/steam_utils.rs +++ b/auth/src/common/steam_utils.rs @@ -30,9 +30,6 @@ fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 { #[cfg(all(feature = "steam", feature = "cardlife"))] const STEAM_ID: &str = "920690"; // Cardlife steam app id -#[cfg(all(feature = "steam", feature = "robocraft"))] -const STEAM_ID: &str = "301520"; // Robocraft steam app id - #[cfg(feature = "steam")] static STEAM_SERVER: std::sync::OnceLock = std::sync::OnceLock::new(); diff --git a/auth/src/main.rs b/auth/src/main.rs index afc6fcd..661c915 100644 --- a/auth/src/main.rs +++ b/auth/src/main.rs @@ -3,8 +3,6 @@ mod common; #[cfg(feature = "cardlife")] mod cardlife; -#[cfg(feature = "robocraft")] -mod robocraft; #[rocket::get("/")] fn index() -> String { @@ -27,12 +25,7 @@ async fn rocket() -> _ { #[cfg(feature = "cardlife")] {builder = builder.attach(cardlife::stage());} - #[cfg(feature = "robocraft")] - {builder = builder.attach(robocraft::stage());} builder } -#[cfg(all(feature = "steam", feature = "robocraft", feature = "cardlife"))] -compile_error!("Feature \"steam\" cannot work with features \"cardlife\" and \"robocraft\" at the same time"); - diff --git a/auth/src/robocraft/debug.rs b/auth/src/robocraft/debug.rs deleted file mode 100644 index f553093..0000000 --- a/auth/src/robocraft/debug.rs +++ /dev/null @@ -1,13 +0,0 @@ -use rocket::{post, routes}; - -#[post("/", data = "")] -pub fn debug_endpoint(body: String) -> String { - log::info!("got body: `{}`", body); - body.to_string() -} - -pub fn stage() -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft debug", |rocket| async { - rocket.mount("/", routes![debug_endpoint]) - }) -} diff --git a/auth/src/robocraft/mod.rs b/auth/src/robocraft/mod.rs deleted file mode 100644 index 3d2cba0..0000000 --- a/auth/src/robocraft/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -mod debug; -mod username; -mod steam; -mod email; -mod registration; - -pub fn stage() -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async { - rocket.attach(username::stage()) - .attach(email::stage()) - .attach(steam::stage()) - .attach(debug::stage()) - .attach(registration::stage()) - .register("/", rocket::catchers![unauthorized]) - }) -} - -#[allow(dead_code)] -pub struct RcConfig { - pub data: std::path::PathBuf, - pub account_provider: oj_rc_core::UserImpl, - pub assets: std::path::PathBuf, -} - -impl RcConfig { - pub async fn from_args(args: &crate::common::cli::CliArgs) -> Self { - let conf = oj_rc_core::persist::config::ConfigImpl::load(&args.assets_robocraft).expect("Bad config data"); - Self { - account_provider: oj_rc_core::UserImpl::load(&args.data_robocraft, &conf).await.expect("Invalid Robocraft user data"), - data: args.data_robocraft.clone().into(), - assets: args.assets_robocraft.clone().into(), - } - } -} - -use rocket::{catch, serde::json::Json}; - -#[catch(401)] -fn unauthorized() -> Json { - Json(libfj::robocraft::ErrorInfo { - error_code: "204".to_owned(), - error_message: "Invalid username, password, or SteamID".to_owned(), - }) -} diff --git a/auth/src/robocraft/steam.rs b/auth/src/robocraft/steam.rs deleted file mode 100644 index 0320fb2..0000000 --- a/auth/src/robocraft/steam.rs +++ /dev/null @@ -1,33 +0,0 @@ -use oj_rc_core::UserAuthenticator; -use rocket::{http::Status, post, routes, serde::json::Json, State}; - -#[post("/authenticate/steam/game", data = "")] -pub async fn steam_auth(body: Json, config: &State) -> Result, Status> { - let steam_id = crate::common::steam_utils::authenticate_steam_ticket(&body.steam_ticket) - .map_err(|_| Status { code: 401 })?; - log::info!("Authenticating {} steam user {}", body.target, steam_id); - let payload = libfj::robocraft::TokenPayload { - public_id: steam_id.to_string(), - display_name: steam_id.to_string(), - robocraft_name: steam_id.to_string(), - email_address: format!("{}.rc.steam@ngram.ca", steam_id), - email_verified: true, - flags: Vec::new(), - }; - let user_info = oj_rc_core::persist::user::UserInfo { - payload, - extra: oj_rc_core::persist::user::ExtraUserInfo::Steam { id: steam_id }, - }; - let response = config.robocraft.account_provider.login(user_info).await - .map_err(|e| { - log::error!("Failed to authenticate {} steam user {}: {}", body.target, steam_id, e); - Status { code: 401 } - })?; - Ok(Json(response.response)) -} - -pub fn stage() -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async { - rocket.mount("/", routes![steam_auth]) - }) -} diff --git a/rc_auth/Cargo.toml b/rc_auth/Cargo.toml new file mode 100644 index 0000000..b0c5773 --- /dev/null +++ b/rc_auth/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rc_auth" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +actix-web.workspace = true +actix-files.workspace = true +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "rt-multi-thread" ] } +clap.workspace = true +oj_rc_core = { version = "*", path = "../rc_core" } +libfj.workspace = true +git-version.workspace = true +serde.workspace = true +serde_json.workspace = true +hex = "0.4" + +handlebars = { version = "5", features = ["dir_source"] } diff --git a/rc_auth/run_debug.sh b/rc_auth/run_debug.sh new file mode 100755 index 0000000..a6e46e5 --- /dev/null +++ b/rc_auth/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_LOG=debug cargo run diff --git a/rc_auth/src/cli.rs b/rc_auth/src/cli.rs new file mode 100644 index 0000000..1b57397 --- /dev/null +++ b/rc_auth/src/cli.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 8001)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Assets root + #[arg(long, default_value_t = {"../assets/robocraft".to_string()})] + pub assets_robocraft: String, + + /// User data root + #[arg(long, default_value_t = {"../data/robocraft".to_string()})] + pub data_robocraft: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } + + pub async fn preloaded(self) -> crate::robocraft::RcConfig { + crate::robocraft::RcConfig::from_args(&self).await + } +} diff --git a/rc_auth/src/main.rs b/rc_auth/src/main.rs new file mode 100644 index 0000000..c0c7b05 --- /dev/null +++ b/rc_auth/src/main.rs @@ -0,0 +1,60 @@ +mod cli; +mod robocraft; + +use actix_web::{App, HttpServer, Responder}; + +#[actix_web::get("/")] +async fn index() -> impl Responder { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]); + let authors = env!("CARGO_PKG_AUTHORS"); + let license = env!("CARGO_PKG_LICENSE"); + let repo = env!("CARGO_PKG_REPOSITORY"); + format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let cli_args = cli::CliArgs::get(); + let cli_args2 = actix_web::web::Data::new(cli_args.clone()); + let rc_preloaded = actix_web::web::Data::new(cli_args.clone().preloaded().await); + + let mut handlebars = handlebars::Handlebars::new(); + handlebars + .register_templates_directory( + std::path::PathBuf::from(&cli_args.assets_robocraft).parent().expect("Bad robocraft asset path").join("templates"), + handlebars::DirectorySourceOptions { + tpl_extension: ".html.hbs".to_owned(), + hidden: false, + temporary: false, + }, + ) + .unwrap(); + let handlebars_ref = actix_web::web::Data::new(handlebars); + + HttpServer::new(move || { + App::new() + .app_data(cli_args2.clone()) + .app_data(rc_preloaded.clone()) + .app_data(handlebars_ref.clone()) + .service(index) + .service(robocraft::registration::form_submit) + .service(robocraft::registration::form_load) + .service(robocraft::registration::favicon) + .service(robocraft::email::email_password_auth) + .service(robocraft::steam::steam_auth) + .service(robocraft::username::user_password_auth) + /*.service(robocraft::live_data::live_data_json) + .service(robocraft::user_avatar::get) + .service(robocraft::clan_avatar::get) + .service(robocraft::brawl_data::get) + .service(robocraft::campaign_data::get) + .service(robocraft::factory::arc::get) + .service(robocraft::favicon::get)*/ + }) + .bind((cli_args.ip, cli_args.port))? + .run() + .await +} diff --git a/auth/src/robocraft/email.rs b/rc_auth/src/robocraft/email.rs similarity index 56% rename from auth/src/robocraft/email.rs rename to rc_auth/src/robocraft/email.rs index 1442679..1e3c52d 100644 --- a/auth/src/robocraft/email.rs +++ b/rc_auth/src/robocraft/email.rs @@ -1,8 +1,8 @@ use oj_rc_core::UserAuthenticator; -use rocket::{post, routes, serde::json::Json, http::Status, State}; +use actix_web::{post, web::{Data, Json}}; -#[post("/authenticate/email/game", data = "")] -pub async fn email_password_auth(body: Json, config: &State) -> Result, Status> { +#[post("/authenticate/email/game")] +pub async fn email_password_auth(body: Json, config: Data) -> Result, super::ErrorTy> { log::info!("Authenticating {} user {}", body.target, body.display_name); let payload = libfj::robocraft::TokenPayload { public_id: body.display_name.clone(), @@ -16,16 +16,10 @@ pub async fn email_password_auth(body: Json rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async { - rocket.mount("/", routes![email_password_auth]) - }) -} diff --git a/rc_auth/src/robocraft/mod.rs b/rc_auth/src/robocraft/mod.rs new file mode 100644 index 0000000..ff478f7 --- /dev/null +++ b/rc_auth/src/robocraft/mod.rs @@ -0,0 +1,63 @@ +pub mod email; +pub mod registration; +pub mod steam; +pub mod username; + +pub struct RcConfig { + //pub data: std::path::PathBuf, + pub account_provider: oj_rc_core::UserImpl, + pub assets: std::path::PathBuf, +} + +impl RcConfig { + pub async fn from_args(args: &crate::cli::CliArgs) -> Self { + let conf = oj_rc_core::persist::config::ConfigImpl::load(&args.assets_robocraft).expect("Bad config data"); + Self { + account_provider: oj_rc_core::UserImpl::load(&args.data_robocraft, &conf).await.expect("Invalid Robocraft user data"), + //data: args.data_robocraft.clone().into(), + assets: args.assets_robocraft.clone().into(), + } + } +} + +pub(self) struct ErrorTy { + json: libfj::robocraft::ErrorInfo, +} + +impl ErrorTy { + pub fn from_err(error: oj_rc_core::persist::user::AuthError) -> Self { + Self { + json: libfj::robocraft::ErrorInfo { + error_code: error.code.to_str(), + error_message: error.message, + }, + } + } +} + +impl actix_web::error::ResponseError for ErrorTy { + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::UNAUTHORIZED + } + + fn error_response(&self) -> actix_web::HttpResponse { + actix_web::HttpResponse::with_body( + self.status_code(), + serde_json::to_string(&self.json).unwrap(), + ).map_into_boxed_body() + } +} + +impl core::fmt::Display for ErrorTy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + //use core::fmt::Write; + write!(f, "({}) {}", self.json.error_code, self.json.error_message) + } +} + +impl core::fmt::Debug for ErrorTy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ErrorTy") + .finish_non_exhaustive() + } +} diff --git a/auth/src/robocraft/registration.rs b/rc_auth/src/robocraft/registration.rs similarity index 62% rename from auth/src/robocraft/registration.rs rename to rc_auth/src/robocraft/registration.rs index c4d7caa..57acfd8 100644 --- a/auth/src/robocraft/registration.rs +++ b/rc_auth/src/robocraft/registration.rs @@ -1,7 +1,7 @@ use oj_rc_core::UserAuthenticator; -use rocket::{post, get, form::{Form, FromForm}, routes, http::Status, State}; -use rocket_dyn_templates::{Template, context}; -use serde::Serialize; +use actix_web::{get, post, web::{Data, Form, Html}, Responder}; +//use rocket_dyn_templates::{Template, context}; +use serde::{Serialize, Deserialize}; const FORM_NAME: &str = "rc_register"; const FORM_NAME_SUCCESS: &str = "rc_register_success"; @@ -12,7 +12,7 @@ const VALID_CHARS: &[char] = &[ '_', ]; -#[derive(FromForm, Serialize)] +#[derive(Serialize, Deserialize)] struct RegisterForm { display_name: String, password: String, @@ -29,6 +29,14 @@ struct Context { source_url: String, } +#[derive(Serialize)] +struct ContextSuccess { + display_name: String, + id: i32, + version: String, + source_url: String, +} + fn version_string() -> String { let name = env!("CARGO_PKG_NAME"); let version = env!("CARGO_PKG_VERSION"); @@ -46,35 +54,37 @@ fn all_valid_chars(s: &str) -> bool { true } -fn registration_ok(form: RegisterForm) -> Template { - Template::render(FORM_NAME, Context { +fn registration_ok(form: RegisterForm, renderer: &handlebars::Handlebars<'_>) -> Html { + let rendered = renderer.render(FORM_NAME, &Context { form, error: None, version: version_string(), source_url: env!("CARGO_PKG_REPOSITORY").to_owned(), - }) + }).unwrap(); + Html::new(rendered) } -fn registration_err(form: RegisterForm, error: String) -> Template { - Template::render(FORM_NAME, Context { +fn registration_err(form: RegisterForm, error: String , renderer: &handlebars::Handlebars<'_>) -> Html { + let rendered = renderer.render(FORM_NAME, &Context { form, error: Some(error), version: version_string(), source_url: env!("CARGO_PKG_REPOSITORY").to_owned(), - }) + }).unwrap(); + Html::new(rendered) } -#[post("/register", data = "
")] -async fn form_submit(form: Form, config: &State) -> Result { +#[post("/register")] +pub async fn form_submit(form: Form, config: Data, handlebars_ref: Data>) -> Result { // password confirmation validation if form.password != form.password_c { - return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned())); + return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned(), &*handlebars_ref)); } if form.password.len() < 8 { - return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned())); + return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned(), &*handlebars_ref)); } if form.password.len() > 128 { - return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned())); + return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned(), &*handlebars_ref)); } // email validation @@ -84,16 +94,16 @@ async fn form_submit(form: Form, config: &State, config: &State id, - Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned())), + Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned(), &*handlebars_ref)), }; if steam_id >= 7656120_0000000000 || steam_id < 7656119_0000000000 { - return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned())); + return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned(), &*handlebars_ref)); } - let steam_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::SteamId(steam_id)) + let steam_exists = config.account_provider.user_exists(oj_rc_core::persist::user::UserId::SteamId(steam_id)) .await .map_err(|e| { log::error!("Failed to check if user steam id {} exists: {}", steam_id, e); - Status { code: 500 } + actix_web::error::ErrorInternalServerError(e) })?; if steam_exists { - return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned())); + return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned(), &*handlebars_ref)); } actual_steam_id = Some(steam_id); } @@ -131,25 +141,25 @@ async fn form_submit(form: Form, config: &State 32 { - return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned())); + return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned(), &*handlebars_ref)); } if !all_valid_chars(&form.display_name.to_lowercase()) { - return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned())); + return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned(), &*handlebars_ref)); } - let username_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::Username(form.display_name.to_owned())) + let username_exists = config.account_provider.user_exists(oj_rc_core::persist::user::UserId::Username(form.display_name.to_owned())) .await .map_err(|e| { log::error!("Failed to check if user name {} exists: {}", form.display_name, e); - Status { code: 500 } + actix_web::error::ErrorInternalServerError(e) })?; if username_exists { - return Ok(registration_err(form.into_inner(), "Username already registered".to_owned())); + return Ok(registration_err(form.into_inner(), "Username already registered".to_owned(), &*handlebars_ref)); } - let user_id = match config.robocraft.account_provider.register(oj_rc_core::persist::user::RegistrationInfo { + let user_id = match config.account_provider.register(oj_rc_core::persist::user::RegistrationInfo { display_name: form.display_name.clone(), password: form.password.clone(), email: actual_email, @@ -157,40 +167,31 @@ async fn form_submit(form: Form, config: &State id, Err(e) => { - return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e))); + return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e), &*handlebars_ref)); } }; - Ok(Template::render(FORM_NAME_SUCCESS, context! { + Ok(Html::new(handlebars_ref.render(FORM_NAME_SUCCESS, &ContextSuccess { display_name: form.display_name.clone(), id: user_id, version: version_string(), source_url: env!("CARGO_PKG_REPOSITORY").to_owned(), - })) + }).unwrap())) } #[get("/register")] -async fn form_load() -> Template { +pub async fn form_load(handlebars_ref: Data>) -> Html { registration_ok(RegisterForm { display_name: "".to_owned(), password: "".to_owned(), password_c: "".to_owned(), email: None, steam_id: None, - }) + }, &*handlebars_ref) } #[get("/robocraft/favicon")] -pub async fn favicon(config: &State) -> Result { - let path = config.robocraft.assets.join("favicon.jpg"); - rocket::fs::NamedFile::open(path).await -} - - - -pub fn stage() -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async { - rocket.mount("/", routes![form_load, form_submit, favicon]) - .attach(Template::fairing()) - }) +pub async fn favicon(config: Data) -> impl Responder { + let path = config.assets.join("favicon.jpg"); + actix_files::NamedFile::open_async(path).await } diff --git a/rc_auth/src/robocraft/steam.rs b/rc_auth/src/robocraft/steam.rs new file mode 100644 index 0000000..c4bdcfa --- /dev/null +++ b/rc_auth/src/robocraft/steam.rs @@ -0,0 +1,65 @@ +use oj_rc_core::UserAuthenticator; +use actix_web::{post, web::{Data, Json}}; + +fn authenticate_steam_ticket(hex_ticket: &str) -> Result { + get_steam_id_from_ticket_hex(hex_ticket) + .map_err(|e| { + log::error!("Failed to parse steamId: {}", e); + () + }) +} + +fn get_steam_id_from_ticket_hex(hex_ticket: &str) -> Result { + let decoded_ticket = hex::decode(hex_ticket)?; + if decoded_ticket.len() < 72 { + Err(hex::FromHexError::InvalidStringLength) + } else { + Ok(get_steam_id_from_ticket(&decoded_ticket)) + } +} + +fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 { + get_u64_with_offset(&ticket, 12 /* also at 64 ??? */) // should be 76600000000000000 > number > 76500000000000000 +} + +fn get_u64_with_offset(arr: &[u8], start: usize) -> u64 { + let arr_actual: [u8; 8] = [ + arr[start], + arr[start+1], + arr[start+2], + arr[start+3], + arr[start+4], + arr[start+5], + arr[start+6], + arr[start+7], + ]; + u64::from_le_bytes(arr_actual) +} + +#[post("/authenticate/steam/game")] +pub async fn steam_auth(body: Json, config: Data) -> Result, super::ErrorTy> { + let steam_id = authenticate_steam_ticket(&body.steam_ticket) + .map_err(|_| super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError { + message: "Bad SteamId".to_owned(), + code: oj_rc_core::data::error_codes::AuthErrorCode::BadCredentials, + }))?; + log::info!("Authenticating {} steam user {}", body.target, steam_id); + let payload = libfj::robocraft::TokenPayload { + public_id: steam_id.to_string(), + display_name: steam_id.to_string(), + robocraft_name: steam_id.to_string(), + email_address: format!("{}.rc.steam@ngram.ca", steam_id), + email_verified: true, + flags: Vec::new(), + }; + let user_info = oj_rc_core::persist::user::UserInfo { + payload, + extra: oj_rc_core::persist::user::ExtraUserInfo::Steam { id: steam_id }, + }; + let response = config.account_provider.login(user_info).await + .map_err(|e| { + log::error!("Failed to authenticate {} steam user {}: {}", body.target, steam_id, e.message); + super::ErrorTy::from_err(e) + })?; + Ok(Json(response.response)) +} diff --git a/auth/src/robocraft/username.rs b/rc_auth/src/robocraft/username.rs similarity index 56% rename from auth/src/robocraft/username.rs rename to rc_auth/src/robocraft/username.rs index 7efedf1..694d213 100644 --- a/auth/src/robocraft/username.rs +++ b/rc_auth/src/robocraft/username.rs @@ -1,8 +1,8 @@ use oj_rc_core::UserAuthenticator; -use rocket::{post, routes, serde::json::Json, http::Status, State}; +use actix_web::{post, web::{Data, Json}}; -#[post("/authenticate/robocraft/game", data = "")] -pub async fn user_password_auth(body: Json, config: &State) -> Result, Status> { +#[post("/authenticate/robocraft/game")] +pub async fn user_password_auth(body: Json, config: Data) -> Result, super::ErrorTy> { log::info!("Authenticating {} user {}", body.target, body.display_name); let payload = libfj::robocraft::TokenPayload { public_id: body.display_name.clone(), @@ -16,16 +16,10 @@ pub async fn user_password_auth(body: Json rocket::fairing::AdHoc { - rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async { - rocket.mount("/", routes![user_password_auth]) - }) -} diff --git a/rc_core/src/data/error_codes.rs b/rc_core/src/data/error_codes.rs index f4ad55d..b78b410 100644 --- a/rc_core/src/data/error_codes.rs +++ b/rc_core/src/data/error_codes.rs @@ -101,3 +101,23 @@ impl LobbyReasonCode { } } } + +#[repr(u16)] // doesn't really matter +#[derive(Debug)] +pub enum AuthErrorCode { + Unknown = 0, + InvalidDisplayName = 122, + AccountBlocked202 = 202, + PasswordInvalidated = 203, + BadCredentials = 204, + DisplayeNameAlreadyInUse = 210, + AccountBlocked301 = 301, + AccountUnconfirmed = 302, + UnlinkedSteamAccount = 303, +} + +impl AuthErrorCode { + pub fn to_str(self) -> String { + (self as u16).to_string() + } +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index dfa0816..6388114 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -63,21 +63,36 @@ impl AccountProvider { #[async_trait::async_trait] impl super::UserProvider for AccountProvider { - async fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, String> { + async fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, super::AuthError> { //let new_root = self.root.join(&token.uuid); let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); validation.set_required_spec_claims::<&str>(&[]); - jsonwebtoken::decode::(&token.token, &secret, &validation).map_err(|e| e.to_string())?; - let user_info = if let Some(user_info) = self.db.user_by_any_unique_id(token.uuid.clone()).await.map_err(|e| e.to_string())? { + jsonwebtoken::decode::(&token.token, &secret, &validation).map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + })?; + let user_info = if let Some(user_info) = self.db.user_by_any_unique_id(token.uuid.clone()).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })? { user_info } else { - return Err("User not found".to_owned()); + return Err(super::AuthError { + message: "User not found".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); }; - let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| e.to_string())? { + let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown + })? { user_perms } else { - return Err("User permissions not found".to_owned()); + return Err(super::AuthError { + message: "User permissions not found".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); }; //let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?; Ok(Box::new(UserData { @@ -90,16 +105,28 @@ impl super::UserProvider for AccountProvider { //Err("Unable to authenticate".to_string()) } - async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, String> { - let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| e.to_string())? { + async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, super::AuthError> { + let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })? { user_info } else { - return Err("User not found".to_owned()); + return Err(super::AuthError { + message: "User not found".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); }; - let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| e.to_string())? { + let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })? { user_perms } else { - return Err("User permissions not found".to_owned()); + return Err(super::AuthError { + message: "User permissions not found".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); }; Ok(Box::new(UserData { account: user_info, @@ -113,14 +140,17 @@ impl super::UserProvider for AccountProvider { #[async_trait::async_trait] impl super::UserAuthenticator for AccountProvider { - async fn login(&self, info: super::UserInfo) -> Result { + async fn login(&self, info: super::UserInfo) -> Result { //let new_root = self.root.join(&info.payload.public_id); let is_new_user; let user_opt = match &info.extra { super::ExtraUserInfo::Steam { id } => self.db.user_by_steam_id(*id).await, super::ExtraUserInfo::Email { .. } => self.db.user_by_email(info.payload.email_address.clone()).await, super::ExtraUserInfo::Username { .. } => self.db.user_by_display_name(info.payload.display_name.clone()).await, - }.map_err(|e| e.to_string())?; + }.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + })?; let mut user_info = if let Some(user_info) = user_opt { is_new_user = false; user_info @@ -128,11 +158,20 @@ impl super::UserAuthenticator for AccountProvider { is_new_user = true; if self.auto_signups { log::info!("New user {}", info.payload.public_id); - super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?; - self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap() + super::setup_new_user(&info, &self.db).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })?; + self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })?.unwrap() } else { log::info!("Rejecting user sign-in for `{}` (set settings.server.auto_signup=true to disable this behaviour)", info.payload.public_id); - return Err(format!("User does not exist")); + return Err(super::AuthError { + message: "User not found".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); } }; let override_password = user_info.password.is_empty() && user_info.steam_id.is_none(); @@ -141,10 +180,16 @@ impl super::UserAuthenticator for AccountProvider { let id_str = id.to_string(); if let Some(expected_steam_id) = user_info.steam_id { if expected_steam_id != id_str { - return Err("SteamID does not match".to_owned()) + return Err(super::AuthError { + message: "SteamID does not match".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); } } else { - return Err("SteamID not supported for this user".to_owned()); + return Err(super::AuthError { + message: "SteamID not supported for this user".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); } }, super::ExtraUserInfo::Email { password } @@ -153,17 +198,46 @@ impl super::UserAuthenticator for AccountProvider { let argon2_algo = argon2::Argon2::default(); if override_password { let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng); - let password_hash = argon2_algo.hash_password(password.as_bytes(), &salt).map_err(|e| e.to_string())?.to_string(); + let password_hash = argon2_algo.hash_password(password.as_bytes(), &salt).map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })?.to_string(); user_info.password = password_hash; } if !user_info.password.is_empty() { - let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| e.to_string())?; - argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| e.to_string())?; + let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })?; + argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })?; } else { - return Err("Password not supported for this user".to_owned()) + return Err(super::AuthError { + message: "Password not supported for this user".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); } } } + // check if user is banned + if let Some(perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError { + message: e.to_string(), + code: crate::data::error_codes::AuthErrorCode::Unknown, + })? { + if perms.banned { + return Err(super::AuthError { + message: "User is banned".to_owned(), + code: crate::data::error_codes::AuthErrorCode::AccountBlocked301, + }); + } + } else { + return Err(super::AuthError { + message: "User has no permissions".to_owned(), + code: crate::data::error_codes::AuthErrorCode::BadCredentials, + }); + } // authentication has now definitely succeeded // build token let header = jsonwebtoken::Header { diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 699caee..c666ede 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,7 +11,7 @@ mod inventory; pub use inventory::UnlockedParts; mod traits; -pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent}; +pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError}; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index d3a9d1f..3cf37e4 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -41,16 +41,21 @@ pub struct RegistrationInfo { pub steam_id: Option, } +pub struct AuthError { + pub message: String, + pub code: crate::data::error_codes::AuthErrorCode, +} + #[async_trait::async_trait] pub trait UserProvider { - async fn authenticate(&self, user: UserToken) -> Result + Send + Sync>, String>; + async fn authenticate(&self, user: UserToken) -> Result + Send + Sync>, AuthError>; - async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, String>; + async fn multiplayer_authenticate(&self, user: String) -> Result + Send + Sync>, AuthError>; } #[async_trait::async_trait] pub trait UserAuthenticator { - async fn login(&self, info: UserInfo) -> Result; + async fn login(&self, info: UserInfo) -> Result; async fn user_exists(&self, user: UserId) -> Result; async fn register(&self, info: RegistrationInfo) -> Result; } diff --git a/rc_core/src/state.rs b/rc_core/src/state.rs index 8fb18b7..5e17a33 100644 --- a/rc_core/src/state.rs +++ b/rc_core/src/state.rs @@ -39,7 +39,7 @@ impl UserState { true }, Err(e) => { - log::error!("Failed to authenticate {}: {}", splits[0], e); + log::error!("Failed to authenticate {}: ({:?}) {}", splits[0], e.code, e.message); false } }