add eula versioning
Some checks failed
Build and Test / account (push) Has been cancelled

This commit is contained in:
red binder 2026-08-09 14:52:45 +02:00
commit d7c226b5a1
5 changed files with 164 additions and 57 deletions

View file

View file

@ -64,6 +64,7 @@ pub struct User {
pub updated: NaiveDateTime,
pub nex_password: String,
pub verification_code: Option<i32>,
pub eula_version: i32,
}
#[derive(sqlx::FromRow)]
@ -170,6 +171,59 @@ pub async fn read_basic_auth_token(
}
}
pub async fn read_hashed_basic_auth_token(
connection: &Pool,
token: &str,
) -> Option<(User, Option<[u8; 32]>)> {
let data = match BASE64_STANDARD.decode(token) {
Ok(d) => d,
Err(e) => {
println!("Failed to decode base64: {:?}", e);
return None;
}
};
let decoded_token = match String::from_utf8(data) {
Ok(s) => s,
Err(e) => {
println!("Failed to convert decoded bytes to UTF-8 string: {:?}", e);
return None;
}
};
let (login_username, login_hash) = match decoded_token.split_once(' ') {
Some(parts) => parts,
None => {
println!("Failed to split hashed basic token into username and hash");
return None;
}
};
let user_result = sqlx::query_as!(
User,
"SELECT * FROM users WHERE username = $1",
login_username
)
.fetch_one(connection)
.await;
let user = match user_result {
Ok(u) => u,
Err(e) => {
println!("Failed to fetch user from database: {:?}", e);
return None;
}
};
let password_valid = user.verify_hashed_password(login_hash);
if password_valid == Some(true) {
Some((user, None))
} else {
None
}
}
pub async fn read_bearer_auth_token(
connection: &Pool,
token: &str,
@ -307,6 +361,7 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
let data = match auth_type {
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
"HashedBasic" if !FORCE_BEARER_AUTH => read_hashed_basic_auth_token(pool, token).await,
"Bearer" => read_bearer_auth_token(pool, token).await,
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
};

View file

@ -1,37 +1,64 @@
use std::{env, io};
use std::collections::HashSet;
use std::{env, io, fs};
use serde::Deserialize;
use once_cell::sync::Lazy;
use rocket::fs::NamedFile;
use rocket::{get, Request};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::get;
use rocket::response::content::RawXml;
use tokio::fs::try_exists;
use tokio::sync::RwLock;
use rocket::async_trait;
use crate::dsresponse::Ds;
pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
// pub static EVIL_AGREEMENT_THING: Lazy<RwLock<HashSet<String>>> = Lazy::new(|| Default::default());
pub struct CFIP(pub String);
// pub struct CFIP(pub String);
#[async_trait]
impl<'r> FromRequest<'r> for CFIP{
type Error = ();
// #[async_trait]
// impl<'r> FromRequest<'r> for CFIP{
// type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get("CF-Connecting-IP").next(){
Some(v) => Outcome::Success(Self(v.to_owned())),
None => Outcome::Error((Status::ImATeapot, ()))
}
}
// async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
// match request.headers().get("CF-Connecting-IP").next(){
// Some(v) => Outcome::Success(Self(v.to_owned())),
// None => Outcome::Error((Status::ImATeapot, ()))
// }
// }
// }
#[derive(Debug, Deserialize)]
struct Agreement {
version: String,
}
#[derive(Debug, Deserialize)]
struct Agreements {
#[serde(rename = "agreement")]
agreements: Vec<Agreement>,
}
pub fn get_latest_eula_version() -> Result<String, Box<dyn std::error::Error>> {
let mut path = std::env::current_dir()?;
path.push("res");
path.push("agreement");
path.push("DEFAULT.xml");
let content = fs::read_to_string(path)?;
let parsed: Agreements = quick_xml::de::from_str(&content)?;
parsed
.agreements
.first()
.map(|a| a.version.clone())
.ok_or_else(|| "No agreement found in XML".into())
}
pub static LATEST_EULA_VERSION: Lazy<i32> = Lazy::new(|| {
get_latest_eula_version()
.ok()
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(300)
});
#[get("/v1/api/content/agreements/Nintendo-Network-EULA/<lang>/@latest")]
pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result<Ds<RawXml<NamedFile>>>{
pub async fn get_agreement(lang: &str) -> io::Result<Ds<RawXml<NamedFile>>>{
let base_path = {
// if this crashes then something is wrong with the server setup so crashing here is fine imo
let mut path = env::current_dir().unwrap();
@ -42,38 +69,26 @@ pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result<Ds<RawXml<NamedFi
path
};
if EVIL_AGREEMENT_THING.read().await.contains(&ip.0) {
let path = {
let requested_file_path = {
let mut path = base_path.clone();
path.push(format!("{}.xml", lang));
path
};
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
} else {
let fallback_path = {
let mut path = base_path;
path.push("EVIL.xml");
path.push("DEFAULT.xml");
path
};
Ok(Ds(RawXml(NamedFile::open(&path).await?)))
} else {
let requested_file_path = {
let mut path = base_path.clone();
path.push(format!("{}.xml", lang));
path
};
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true) {
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
} else {
let fallback_path = {
let mut path = base_path;
path.push("DEFAULT.xml");
path
};
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
}
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
}
}

View file

@ -2,9 +2,9 @@
use crate::Pool;
use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid};
use crate::error::{Error, Errors};
use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING};
use crate::nnid::oauth::TokenData;
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
use crate::nnid::agreements::LATEST_EULA_VERSION;
use crate::xml::Xml;
use rocket::form::Form;
use rocket::{FromForm, State, post};
@ -30,10 +30,17 @@ const ACCOUNT_BANNED_ERRORS: Errors = Errors {
}],
};
const REREAD_EULA_EXTRABANNED_ERRORS: Errors = Errors {
const ACCOUNT_TEMPBANNED_ERRORS: Errors = Errors {
error: &[Error {
code: "0132",
message: "Account temporarily banned from server",
}],
};
const REREAD_EULA_ERRORS: Errors = Errors {
error: &[Error {
code: "0109",
message: "REREAD THE EULA LOL",
message: "The EULA has been updated",
}],
};
#[derive(FromForm)]
@ -117,7 +124,6 @@ pub struct TokenRequestReturnData {
pub async fn generate_token(
pool: &State<Pool>,
data: Form<TokenRequestData<'_>>,
ip: CFIP,
cert: DeviceCert,
) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>> {
let pool = pool.inner();
@ -133,14 +139,13 @@ pub async fn generate_token(
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
}
if user.eula_version != *LATEST_EULA_VERSION {
return Err(Some(REREAD_EULA_ERRORS));
}
if user.account_level < 0 {
if user.account_level == -2 {
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
}
if user.account_level == -3 {
EVIL_AGREEMENT_THING.write().await.insert(ip.0);
return Err(Some(REREAD_EULA_EXTRABANNED_ERRORS));
return Err(Some(ACCOUNT_TEMPBANNED_ERRORS));
}
return Err(Some(ACCOUNT_BANNED_ERRORS));
}

View file

@ -17,7 +17,7 @@ use rocket::{State, get, post, put};
const DATABASE_ERROR: Errors = Errors {
error: &[Error {
code: "9999",
code: "2001",
message: "Internal server error",
}],
};
@ -494,5 +494,37 @@ pub async fn change_mii(
Ok(())
}
#[post("/v1/api/people/@me/agreements")]
pub async fn thing() {}
#[derive(Deserialize, Debug)]
pub struct AgreedEulaData {
pub agreement_date: NaiveDateTime,
pub country: Box<str>,
pub location: Box<str>,
pub version: i32
}
#[post("/v1/api/people/@me/agreements", data = "<data>")]
pub async fn thing(
database: &State<Pool>,
auth: Auth<false>,
data: Xml<AgreedEulaData>,
) -> Result<(), Option<Errors<'static>>> {
let db = database.inner();
let version = data.version;
let pid = auth.pid;
println!("eula data: {:?}", data.version);
let result = sqlx::query!(
"UPDATE users SET eula_version = $1 WHERE pid = $2",
version,
pid
)
.execute(db)
.await;
if let Err(e) = result {
println!("Failed to update EULA version for PID {}: {:?}", pid, e);
return Err(Some(DATABASE_ERROR));
}
Ok(())
}