feat: stuff

This commit is contained in:
DJMrTV 2025-02-27 21:49:37 +01:00
commit 6d58fd47a1
11 changed files with 244 additions and 366 deletions

View file

@ -5,6 +5,7 @@ use argon2::password_hash::SaltString;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use bytemuck::bytes_of;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use log::{error, warn};
use rocket::http::Status;
use rocket::{async_trait, Request};
@ -34,18 +35,21 @@ const INVALID_TOKEN_ERRORS: Errors<'static> = Errors{
// optimization note: add token caching
pub struct User {
pub id: i32,
pub name: String,
pub pid: i32,
pub username: String,
pub password: String,
pub birthdate: chrono::NaiveDate,
pub birthdate: NaiveDate,
pub timezone: String,
pub email: String,
pub account_level: i32,
pub email_verified_since: Option<NaiveDateTime>,
pub gender: String,
pub country: String,
pub language: String,
pub marketing_allowed: bool,
pub off_device_allowed: bool,
pub region: i32,
pub mii_identifier: String
pub mii_data: String
}
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
@ -59,40 +63,39 @@ fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
}
impl User{
fn generate_nintendo_hash(&self, text_password: &str) -> String{
generate_nintendo_hash(self.id, text_password)
pub async fn get_by_username(name: &str, pool: &Pool) -> Option<Self>{
sqlx::query_as!(
Self,
"SELECT * FROM users WHERE username = $1",
name
).fetch_one(pool)
.await
.ok()
}
pub fn verify_password(&self, cleartext_password: &str) -> (bool, bool){
let Ok(hash) = PasswordHash::new(&self.password) else {
error!("invalid password in database for user with pid: {}", self.id);
fn generate_nintendo_hash(&self, text_password: &str) -> String{
generate_nintendo_hash(self.pid, text_password)
}
if self.password == self.generate_nintendo_hash(cleartext_password){
return (true, true)
}
pub fn verify_cleartext_password(&self, cleartext_password: &str) -> Option<bool>{
let nintendo_hash = self.generate_nintendo_hash(cleartext_password);
return (false, false)
};
self.verify_hashed_password(cleartext_password)
}
let argon = Argon2::default();
(argon.verify_password(cleartext_password.as_bytes(), &hash).is_ok(), false)
pub fn verify_hashed_password(&self, hashed_password: &str) -> Option<bool>{
bcrypt::verify(hashed_password, &self.password).ok()
}
}
pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String>{
let password = generate_nintendo_hash(pid, cleartext_password);
let salt = SaltString::generate(&mut OsRng);
let argon = Argon2::default();
let pw = argon.hash_password(password.as_bytes(), &salt).ok()?;
Some(pw.to_string())
bcrypt::hash(password, 10).ok()
}
pub async fn read_basic_auth_token(connection: &mut AsyncMysqlConnection, token: &str) -> Option<User> {
pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = BASE64_STANDARD.decode(&token).ok()?;
let decoded_basic_token = String::from_utf8(data).ok()?;
@ -101,28 +104,22 @@ pub async fn read_basic_auth_token(connection: &mut AsyncMysqlConnection, token:
let mut user: User = users
.filter(name.eq(login_username))
.select(User::as_select())
.first(connection)
.await.ok()?;
let mut user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE username = $1",
login_username
).fetch_one(connection).await.ok()?;
let (password_valid, upgrade_password) = user.verify_password(&login_password);
if password_valid{
if upgrade_password{
user.password = generate_password(&login_password).unwrap();
user = connection.update_and_fetch(&user).await.ok()?;
}
let password_valid = user.verify_cleartext_password(&login_password);
if password_valid == Some(true){
Some(user)
} else {
None
}
}
async fn read_bearer_auth_token(connection: &mut AsyncMysqlConnection, token: &str) -> Option<User> {
async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = BASE64_STANDARD.decode(&token).ok()?;
warn!("bearer token login currently unsupported");
@ -137,15 +134,13 @@ impl<'r> FromRequest<'r> for User{
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let pool: &Pool = request.rocket().state().unwrap();
let mut connection = pool.get().await.unwrap();
let auth = request_try!(request.headers().get("Authorization").next().ok_or(INVALID_TOKEN_ERRORS));
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
let user = match auth_type{
"Basic" => read_basic_auth_token(&mut connection, token).await,
"Bearer" => read_bearer_auth_token(&mut connection, token).await,
"Basic" => read_basic_auth_token(pool, token).await,
"Bearer" => read_bearer_auth_token(pool, token).await,
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
};

5
src/data_wrapper.rs Normal file
View file

@ -0,0 +1,5 @@
use serde::{Deserialize, Serialize};
trait DataWrapper{
}

View file

@ -15,6 +15,7 @@ mod nnid;
mod account;
mod error;
mod dsresponse;
mod data_wrapper;
type Pool = sqlx::Pool<Postgres>;
@ -42,7 +43,7 @@ async fn launch() -> _ {
.to_string()
));
response.adjoin_header(Header::new("Content-Type", "text/xml; charset=utf-8"));
//response.adjoin_header(Header::new("Content-Type", "text/xml; charset=utf-8"));
response.remove_header("x-content-type-options");
@ -55,6 +56,8 @@ async fn launch() -> _ {
nnid::agreements::get_agreement,
nnid::timezones::get_timezone,
nnid::person_exists::person_exists,
nnid::email::validate
nnid::email::validate,
nnid::create_account::create_account,
nnid::oauth::generate_token::generate_token
])
}

View file

@ -1,18 +1,21 @@
use chrono::NaiveDate;
use chrono::{Datelike, NaiveDate};
use rocket::{post, State};
use serde::{Deserialize, Serialize};
use crate::account::account::{generate_password, User};
use crate::error::Errors;
use crate::nnid::pid_distribution::next_pid;
use crate::Pool;
use crate::xml::{Xml, YesNoVal};
#[derive(Deserialize)]
struct Email{
pub struct Email{
address: Box<str>
}
#[derive(Deserialize, Serialize)]
struct Mii{
pub struct Mii{
name: Box<str>,
primary: YesNoVal,
data: Box<str>,
@ -20,7 +23,7 @@ struct Mii{
#[derive(Deserialize)]
#[serde(rename(serialize = "person"))]
struct AccountCreationData{
pub struct AccountCreationData{
birth_date: NaiveDate,
user_id: Box<str>,
password: Box<str>,
@ -28,21 +31,28 @@ struct AccountCreationData{
language: Box<str>,
tz_name: Box<str>,
email: Email,
mii: Mii,
gender: Box<str>,
marketing_flag: YesNoVal,
off_device_flag: YesNoVal,
region: i32
}
#[derive(Serialize)]
#[serde(rename(serialize = "person"))]
struct AccountCreationResponseData{
pub struct AccountCreationResponseData{
pid: i32
}
#[post("/v1/api/people", data="<data>")]
async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Errors>{
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<Errors>>{
let database = database.inner();
// its fine to crash here if we cant get the next pid as that is in my opinion a dead state
// anyways as noone can register anymore, EVER
let pid = next_pid(database).await;
let AccountCreationData {
user_id,
password,
@ -52,22 +62,27 @@ async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>)
email: Email{
address
},
mii: Mii{
data,
..
},
marketing_flag,
gender,
region,
country,
off_device_flag,
..
} = *data;
} = data.0;
let password = generate_password(pid, &password).ok_or(None)?;
let new_account = sqlx::query("
INSERT INTO users.users (
sqlx::query!("
INSERT INTO users (
pid,
username,
password,
birthdate,
birthdate,
timezone,
email,
country,
@ -75,26 +90,34 @@ async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>)
marketing_allowed,
off_device_allowed,
region,
gender,
mii_data
) VALUES (
?,?,?,?,?,?,?,?,?,?
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13
)
");
",
pid,
user_id.as_ref(),
password,
birth_date,
tz_name.as_ref(),
address.as_ref(),
country.as_ref(),
language.as_ref(),
marketing_flag.0,
off_device_flag.0,
region,
gender.as_ref(),
data.as_ref()
).execute(database).await.unwrap();
let pid = connection.transaction::<_, diesel::result::Error, _>(|conn| Box::pin(async move{
use crate::schema::users::dsl::*;
diesel::insert_into(users)
.values(&new_account)
.returning(User::as_returning())
.get_result(conn)
.await?;
Ok(())
})).await;
Ok(
Xml(AccountCreationResponseData{
pid
})
)
}
#[cfg(test)]

View file

@ -3,4 +3,6 @@ pub mod agreements;
pub mod timezones;
pub mod person_exists;
pub mod email;
mod create_account;
pub mod create_account;
pub mod oauth;
mod pid_distribution;

View file

@ -0,0 +1,52 @@
use rocket::{post, FromForm, State};
use rocket::form::Form;
use serde::Deserialize;
use crate::account::account::User;
use crate::error::{Error, Errors};
use crate::Pool;
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors{
error: &[
Error{
code: "0106",
message: "Invalid account ID or password"
}
]
};
const ACCOUNT_BANNED_ERRORS: Errors = Errors{
error: &[
Error{
code: "0122",
message: "Device has been banned by game server"
}
]
};
#[derive(FromForm)]
pub struct TokenRequestData<'a>{
grant_type: &'a str,
user_id: &'a str,
password: &'a str,
password_type: &'a str,
}
#[post("/v1/api/oauth20/access_token/generate", data="<data>")]
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>) -> Result<(), Option<Errors<'static>>>{
let pool = pool.inner();
let user = User::get_by_username(data.user_id, pool).await
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
if !user.verify_hashed_password(&data.password).is_some_and(|v| v){
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
}
if user.account_level < 0{
return Err(Some(ACCOUNT_BANNED_ERRORS));
}
Ok(())
}

1
src/nnid/oauth/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod generate_token;

View file

@ -4,16 +4,19 @@ use crate::error::{Error, Errors};
use crate::Pool;
use crate::xml::Xml;
#[get("/v1/api/people/<username>")]
pub async fn person_exists(database: &State<Pool>, username: &str) -> Result<(), Errors<'static>>{
let database = database.inner();
let exists: bool = sqlx::query_as!(
bool,
"SELECT EXISTS(SELECT 1 FROM users.users WHERE username = ? )",
let exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 ) as exists",
username
).fetch_one(database)
.await
.ok()
.map(|v| v.exists)
.flatten()
.unwrap_or(true);
if exists {

View file

@ -0,0 +1,29 @@
use crate::Pool;
pub async fn next_pid(pool: &Pool) -> i32{
loop {
let next_pid = sqlx::query!("SELECT nextval('pid_counter') as pid")
.fetch_one(pool)
.await
.expect("unable to get next pid")
.pid
.expect("unable to get next pid") as i32;
let already_exists = sqlx::query!(
"SELECT EXISTS(select 1 from users where pid = $1)",
next_pid
).fetch_one(pool)
.await
.ok()
.map(|v| v.exists)
.flatten()
.unwrap_or(true);
if !already_exists{
break next_pid;
}
}
}