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

Add basic RC steam and user/pass authentication

This commit is contained in:
NGnius (Graham)
2025-01-27 19:52:57 -05:00
parent 82d0c3d0f3
commit 49357b2759
23 changed files with 488 additions and 86 deletions

View File

@@ -5,7 +5,9 @@ edition = "2021"
[features]
steam = ["steamworks"]
default = []
robocraft = []
cardlife = []
default = ["robocraft", "cardlife"]
[dependencies]
rocket.workspace = true
@@ -15,3 +17,4 @@ env_logger.workspace = true
uuid = { version = "1.12", features = [ "v4", "fast-rng" ] }
steamworks = { version = "0.11", optional = true }
hex = "0.4"
jsonwebtoken = "9"

View File

@@ -6,7 +6,7 @@ mod temporary_get_user_id;
mod token;
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("JSON", |rocket| async {
rocket::fairing::AdHoc::on_ignite("cardlife", |rocket| async {
rocket.attach(email::stage())
.attach(steam::stage())
.attach(temporary_get_user_id::stage())

View File

@@ -1,86 +1,34 @@
use rocket::{post, routes, serde::json::Json, http::Status};
#[cfg(feature = "steam")]
static STEAM_SERVER: std::sync::OnceLock<steamworks::Server> = OnceLock::new();
#[cfg(feature = "steam")]
fn init_steam() -> steamworks::Server {
let (server, single_client) = steamworks::Server::init(core::net::Ipv4Addr::new(127, 0, 0, 1), 9000, 9001, steamworks::ServerMode::NoAuthentication, "")
.expect("Steam is unavailable");
std::thread::spawn(move || {
loop {
single_client.run_callbacks();
std::thread::sleep(std::time::Duration::from_millis(10));
}
});
server.set_product("920690");
server.log_on_anonymous();
server
}
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("/api/auth/steamauthenticate", data = "<body>")]
pub fn steam_auth(body: Json<libfj::cardlife::SteamAuthenticationPayload>) -> Result<Json<libfj::cardlife::AuthenticationInfo>, Status> {
log::debug!("steam ticket: {}", body.steam_ticket);
match hex::decode(&body.steam_ticket) {
Ok(ticket) => {
if ticket.len() < 72 {
return Err(Status { code: 400 })
}
let steam_id = get_u64_with_offset(&ticket, 12 /* also at 64 ??? */); // should be 76600000000000000 > number > 76500000000000000
log::debug!("Found steamId {}", steam_id);
#[cfg(feature = "steam")]
{
let steam = STEAM_SERVER.get_or_init(init_steam);
if let Err(e) = steam.begin_authentication_session(steamworks::SteamId::from_raw(steam_id), &ticket) {
log::error!("steam server auth session error: {}", e);
return Err(Status { code: 400 })
}
}
Ok(Json(libfj::cardlife::AuthenticationInfo {
public_id: uuid::Uuid::from_u64_pair(steam_id, steam_id).to_string(),
email_address: "nobody@openjamgames.com".to_string(),
display_name: steam_id.to_string(),
purchases: vec![1, 2, 3],
flags: vec![],
/*flags: //vec![
"Dev".to_string(),
"GiveInv".to_string(),
"NoDrop".to_string(),
"DekStruct".to_string(),
"BucketA".to_string(),
"BucketB".to_string(),
"userlogged".to_string(),
],*/
confirmed: true,
token: uuid::Uuid::from_u64_pair(steam_id, steam_id).to_string(),
steam_id: Some(steam_id.to_string()),
id: (steam_id & (i32::MAX as u64)) as i32,
}))
},
Err(e) => {
log::error!("request with bad steam ticket: {}", e);
Err(Status { code: 400 })
}
}
let steam_id = crate::common::steam_utils::authenticate_steam_ticket(&body.steam_ticket)
.map_err(|_| Status { code: 400 })?;
log::debug!("Found steamId {}", steam_id);
Ok(Json(libfj::cardlife::AuthenticationInfo {
public_id: uuid::Uuid::from_u64_pair(steam_id, steam_id).to_string(),
email_address: "nobody@openjamgames.com".to_string(),
display_name: steam_id.to_string(),
purchases: vec![1, 2, 3],
flags: vec![],
/*flags: //vec![
"Dev".to_string(),
"GiveInv".to_string(),
"NoDrop".to_string(),
"DelStruct".to_string(),
"BucketA".to_string(),
"BucketB".to_string(),
"userlogged".to_string(),
],*/
confirmed: true,
token: uuid::Uuid::from_u64_pair(steam_id, steam_id).to_string(),
steam_id: Some(steam_id.to_string()),
id: (steam_id & (i32::MAX as u64)) as i32,
}))
}
pub fn stage() -> rocket::fairing::AdHoc {
#[cfg(feature = "steam")]
STEAM_SERVER.get_or_init(init_steam);
rocket::fairing::AdHoc::on_ignite("CardLife Steam", |rocket| async {
rocket.mount("/", routes![steam_auth])
})

1
auth/src/common/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod steam_utils;

View File

@@ -0,0 +1,90 @@
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)
}
#[allow(dead_code)]
fn get_steam_id_from_ticket_hex(hex_ticket: &str) -> Result<u64, hex::FromHexError> {
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))
}
}
#[allow(dead_code)]
fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 {
get_u64_with_offset(&ticket, 12 /* also at 64 ??? */) // should be 76600000000000000 > number > 76500000000000000
}
#[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();
#[cfg(feature = "steam")]
fn init_steam() -> steamworks::Server {
if let Err(e) = std::fs::write::<&str, &[u8]>("./steam_appid.txt", STEAM_ID.as_ref()) {
log::error!("Failed to write appId to steam_appid.txt: {}", e);
}
let (server, single_client) = steamworks::Server::init(core::net::Ipv4Addr::new(127, 0, 0, 1), 9000, 9001, steamworks::ServerMode::NoAuthentication, "")
.expect("Steam is unavailable");
std::thread::spawn(move || {
loop {
single_client.run_callbacks();
std::thread::sleep(std::time::Duration::from_millis(10));
}
});
server.set_product(STEAM_ID);
server.log_on_anonymous();
server
}
#[cfg(feature = "steam")]
fn get_steam() -> &'static steamworks::Server {
STEAM_SERVER.get_or_init(init_steam)
}
#[cfg(not(feature = "steam"))]
pub fn authenticate_steam_ticket(hex_ticket: &str) -> Result<u64, ()> {
get_steam_id_from_ticket_hex(hex_ticket)
.map_err(|e| {
log::error!("Failed to parse steamId: {}", e);
()
})
}
#[cfg(feature = "steam")]
pub fn authenticate_steam_ticket(hex_ticket: &str) -> Result<u64, ()> {
let decoded_ticket = hex::decode(hex_ticket)
.map_err(|e| {
log::error!("Failed to decode hexadecimal steam ticket: {}", e);
()
})?;
if decoded_ticket.len() < 72 {
log::error!("Failed to parse steamId: ticket too short");
return Err(())
}
let steam_id = get_steam_id_from_ticket(&decoded_ticket);
let steam = get_steam();
steam.begin_authentication_session(steamworks::SteamId::from_raw(steam_id), &decoded_ticket)
.map_err(|e| {
log::error!("steam server auth session error: {}", e);
()
})?;
Ok(steam_id)
}

View File

@@ -1,4 +1,9 @@
mod common;
#[cfg(feature = "cardlife")]
mod cardlife;
#[cfg(feature = "robocraft")]
mod robocraft;
#[rocket::get("/")]
fn index() -> &'static str {
@@ -8,7 +13,17 @@ fn index() -> &'static str {
#[rocket::launch]
fn rocket() -> _ {
env_logger::init();
rocket::build().mount("/", rocket::routes![index])
.attach(cardlife::stage())
#[allow(unused_mut)]
let mut builder = rocket::build().mount("/", rocket::routes![index]);
#[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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,39 @@
use rocket::{post, routes, serde::json::Json, http::Status};
fn generate_token(user_auth: &libfj::robocraft::EmailUserAuthenticationPayload) -> String {
let header = jsonwebtoken::Header {
typ: Some("JWT".to_string()),
alg: jsonwebtoken::Algorithm::HS256,
..Default::default()
};
let payload = libfj::robocraft::TokenPayload {
public_id: user_auth.display_name.to_owned(),
display_name: user_auth.display_name.to_owned(),
robocraft_name: user_auth.display_name.to_owned(),
email_address: user_auth.email_address.to_owned(),
email_verified: true,
flags: Vec::new(),
};
let secret = jsonwebtoken::EncodingKey::from_secret(user_auth.password.as_bytes()); // FIXME use an actually secret secret
jsonwebtoken::encode(&header, &payload, &secret)
.unwrap_or_else(|e| {
log::error!("Failed to encode JWT: {}", e);
libfj::robocraft::DEFAULT_TOKEN.to_owned()
})
}
#[post("/authenticate/robocraft/game", data = "<body>")]
pub fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
log::info!("Authenticating {} user {}", body.target, body.display_name);
Ok(Json(libfj::robocraft::AuthenticationResponseInfo {
token: generate_token(&body),
refresh_token: "qwertyuiop".to_string(), // TODO
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
}))
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("Robocraft Email/Password", |rocket| async {
rocket.mount("/", routes![email_password_auth])
})
}

11
auth/src/robocraft/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
mod debug;
mod email;
mod steam;
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async {
rocket.attach(email::stage())
.attach(steam::stage())
.attach(debug::stage())
})
}

View File

@@ -0,0 +1,41 @@
use rocket::{post, routes, serde::json::Json, http::Status};
fn generate_token(user_auth: &libfj::robocraft::SteamAuthenticationPayload, steam_id: u64) -> String {
let header = jsonwebtoken::Header {
typ: Some("JWT".to_string()),
alg: jsonwebtoken::Algorithm::HS256,
..Default::default()
};
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@ngni.us", steam_id),
email_verified: true,
flags: Vec::new(),
};
let secret = jsonwebtoken::EncodingKey::from_secret(user_auth.steam_ticket.as_ref()); // FIXME use an actually secret secret
jsonwebtoken::encode(&header, &payload, &secret)
.unwrap_or_else(|e| {
log::error!("Failed to encode JWT: {}", e);
libfj::robocraft::DEFAULT_TOKEN.to_owned()
})
}
#[post("/authenticate/steam/game", data = "<body>")]
pub fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>) -> 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);
Ok(Json(libfj::robocraft::AuthenticationResponseInfo {
token: generate_token(&body, steam_id),
refresh_token: "qwertyuiop".to_string(), // TODO
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
}))
}
pub fn stage() -> rocket::fairing::AdHoc {
rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async {
rocket.mount("/", routes![steam_auth])
})
}

View File

@@ -1 +0,0 @@
920690