diff --git a/res/agreement/EVIL.xml b/res/agreement/EVIL.xml deleted file mode 100644 index e69de29..0000000 diff --git a/src/account/account.rs b/src/account/account.rs index 41c285a..5dd5fe0 100644 --- a/src/account/account.rs +++ b/src/account/account.rs @@ -64,6 +64,7 @@ pub struct User { pub updated: NaiveDateTime, pub nex_password: String, pub verification_code: Option, + 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)), }; diff --git a/src/nnid/agreements.rs b/src/nnid/agreements.rs index cc97c70..4fa2d59 100644 --- a/src/nnid/agreements.rs +++ b/src/nnid/agreements.rs @@ -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>> = Lazy::new(|| Default::default()); +// pub static EVIL_AGREEMENT_THING: Lazy>> = 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 { - 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 { +// 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, +} + +pub fn get_latest_eula_version() -> Result> { + 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 = Lazy::new(|| { + get_latest_eula_version() + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(300) +}); #[get("/v1/api/content/agreements/Nintendo-Network-EULA//@latest")] -pub async fn get_agreement(lang: &str, ip: CFIP) -> io::Result>>{ - - +pub async fn get_agreement(lang: &str) -> io::Result>>{ 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, data: Form>, - ip: CFIP, cert: DeviceCert, ) -> Result, Option>> { 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)); } diff --git a/src/nnid/people.rs b/src/nnid/people.rs index 211293a..8154392 100644 --- a/src/nnid/people.rs +++ b/src/nnid/people.rs @@ -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, + pub location: Box, + pub version: i32 +} + +#[post("/v1/api/people/@me/agreements", data = "")] +pub async fn thing( + database: &State, + auth: Auth, + data: Xml, +) -> Result<(), Option>> { + 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(()) +}