1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Move Robocraft auth server to actix_web for better error responses #31

This commit is contained in:
NG (Graham)
2025-07-27 16:12:20 -04:00
parent 796304bc4a
commit 89b1db35ba
23 changed files with 455 additions and 211 deletions

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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<steamworks::Server> = std::sync::OnceLock::new();

View File

@@ -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");

View File

@@ -1,13 +0,0 @@
use rocket::{post, routes};
#[post("/", data = "<body>")]
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])
})
}

View File

@@ -1,31 +0,0 @@
use oj_rc_core::UserAuthenticator;
use rocket::{post, routes, serde::json::Json, http::Status, State};
#[post("/authenticate/email/game", data = "<body>")]
pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
log::info!("Authenticating {} user {}", body.target, body.display_name);
let payload = libfj::robocraft::TokenPayload {
public_id: body.display_name.clone(),
display_name: body.display_name.clone(),
robocraft_name: body.display_name.clone(),
email_address: body.email_address.clone(),
email_verified: true,
flags: Vec::new(),
};
let user_info = oj_rc_core::persist::user::UserInfo {
payload,
extra: oj_rc_core::persist::user::ExtraUserInfo::Email { password: body.password.clone() },
};
let response = config.robocraft.account_provider.login(user_info).await
.map_err(|e| {
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
Status { code: 401 }
})?;
Ok(Json(response.response))
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async {
rocket.mount("/", routes![email_password_auth])
})
}

View File

@@ -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<libfj::robocraft::ErrorInfo> {
Json(libfj::robocraft::ErrorInfo {
error_code: "204".to_owned(),
error_message: "Invalid username, password, or SteamID".to_owned(),
})
}

View File

@@ -1,196 +0,0 @@
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;
const FORM_NAME: &str = "rc_register";
const FORM_NAME_SUCCESS: &str = "rc_register_success";
const VALID_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_',
];
#[derive(FromForm, Serialize)]
struct RegisterForm {
display_name: String,
password: String,
password_c: String,
email: Option<String>,
steam_id: Option<String>,
}
#[derive(Serialize)]
struct Context {
form: RegisterForm,
error: Option<String>,
version: String,
source_url: String,
}
fn version_string() -> String {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
//let license = env!("CARGO_PKG_LICENSE");
//let repo = env!("CARGO_PKG_REPOSITORY");
format!("OpenJam {} {}", name, version)
}
fn all_valid_chars(s: &str) -> bool {
for c in s.chars() {
if !VALID_CHARS.contains(&c) {
return false;
}
}
true
}
fn registration_ok(form: RegisterForm) -> Template {
Template::render(FORM_NAME, Context {
form,
error: None,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}
fn registration_err(form: RegisterForm, error: String) -> Template {
Template::render(FORM_NAME, Context {
form,
error: Some(error),
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}
#[post("/register", data = "<form>")]
async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli::Config>) -> Result<Template, Status> {
// password confirmation validation
if form.password != form.password_c {
return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned()));
}
if form.password.len() < 8 {
return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned()));
}
if form.password.len() > 128 {
return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned()));
}
// email validation
let actual_email: Option<String>;
if let Some(email) = &form.email {
if email.is_empty() {
actual_email = None;
} else {
if !email.contains('@') {
return Ok(registration_err(form.into_inner(), "Email must contain @".to_owned()));
}
let email_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::Email(email.to_owned()))
.await
.map_err(|e| {
log::error!("Failed to check if user email {} exists: {}", email, e);
Status { code: 500 }
})?;
if email_exists {
return Ok(registration_err(form.into_inner(), "Email already registered".to_owned()));
}
actual_email = Some(email.to_owned());
}
} else {
actual_email = None;
}
// steam id validation
let actual_steam_id: Option<u64>;
if let Some(steam_id) = &form.steam_id {
if steam_id.is_empty() {
actual_steam_id = None;
} else {
let steam_id = match steam_id.parse() {
Ok(id) => id,
Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned())),
};
if steam_id >= 7656120_0000000000 || steam_id < 7656119_0000000000 {
return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned()));
}
let steam_exists = config.robocraft.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 }
})?;
if steam_exists {
return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned()));
}
actual_steam_id = Some(steam_id);
}
} else {
actual_steam_id = None;
}
// username validation
if form.display_name.len() < 4 {
return Ok(registration_err(form.into_inner(), "Username too short (minimum 4 characters)".to_owned()));
}
if form.display_name.len() > 32 {
return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned()));
}
if !all_valid_chars(&form.display_name.to_lowercase()) {
return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned()));
}
let username_exists = config.robocraft.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 }
})?;
if username_exists {
return Ok(registration_err(form.into_inner(), "Username already registered".to_owned()));
}
let user_id = match config.robocraft.account_provider.register(oj_rc_core::persist::user::RegistrationInfo {
display_name: form.display_name.clone(),
password: form.password.clone(),
email: actual_email,
steam_id: actual_steam_id,
}).await {
Ok(id) => id,
Err(e) => {
return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e)));
}
};
Ok(Template::render(FORM_NAME_SUCCESS, context! {
display_name: form.display_name.clone(),
id: user_id,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
}))
}
#[get("/register")]
async fn form_load() -> Template {
registration_ok(RegisterForm {
display_name: "".to_owned(),
password: "".to_owned(),
password_c: "".to_owned(),
email: None,
steam_id: None,
})
}
#[get("/robocraft/favicon")]
pub async fn favicon(config: &State<crate::common::cli::Config>) -> Result<rocket::fs::NamedFile, std::io::Error> {
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())
})
}

View File

@@ -1,33 +0,0 @@
use oj_rc_core::UserAuthenticator;
use rocket::{http::Status, post, routes, serde::json::Json, State};
#[post("/authenticate/steam/game", data = "<body>")]
pub async fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, 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])
})
}

View File

@@ -1,31 +0,0 @@
use oj_rc_core::UserAuthenticator;
use rocket::{post, routes, serde::json::Json, http::Status, State};
#[post("/authenticate/robocraft/game", data = "<body>")]
pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
log::info!("Authenticating {} user {}", body.target, body.display_name);
let payload = libfj::robocraft::TokenPayload {
public_id: body.display_name.clone(),
display_name: body.display_name.clone(),
robocraft_name: body.display_name.clone(),
email_address: body.email_address.clone(),
email_verified: true,
flags: Vec::new(),
};
let user_info = oj_rc_core::persist::user::UserInfo {
payload,
extra: oj_rc_core::persist::user::ExtraUserInfo::Username { password: body.password.clone() },
};
let response = config.robocraft.account_provider.login(user_info).await
.map_err(|e| {
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
Status { code: 401 }
})?;
Ok(Json(response.response))
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async {
rocket.mount("/", routes![user_password_auth])
})
}