Basically I removed all the warnings, removed some old APIs no longer in use, added cert verification to DB, im just cool like that.
This commit is contained in:
parent
c06afde7cb
commit
5a8e61c255
18 changed files with 363 additions and 254 deletions
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use std::io::{Cursor, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
// Don't import until required.
|
||||
|
|
@ -60,6 +61,18 @@ pub struct User {
|
|||
pub verification_code: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct CertificateRecord {
|
||||
pub _hash: Vec<u8>,
|
||||
pub banned: bool,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct _CertificatePid {
|
||||
pub cert_hash: Vec<u8>,
|
||||
pub pid: i32,
|
||||
}
|
||||
|
||||
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String {
|
||||
let mut sha = Sha256::new();
|
||||
|
||||
|
|
@ -239,6 +252,65 @@ impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> Into<User>
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_certificate(
|
||||
pool: &sqlx::PgPool,
|
||||
cert: &Certificate,
|
||||
pid: i32,
|
||||
) -> Result<(), Errors<'static>> {
|
||||
let hash = cert.hash();
|
||||
|
||||
let existing = sqlx::query_as::<_, CertificateRecord>(
|
||||
"SELECT hash, banned FROM certificates WHERE hash = $1"
|
||||
)
|
||||
.bind(&hash[..])
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
|
||||
if let Some(cert_row) = existing {
|
||||
if cert_row.banned {
|
||||
return Err(INVALID_TOKEN_ERRORS);
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO certificate_pids (cert_hash, pid)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING"
|
||||
)
|
||||
.bind(&hash[..])
|
||||
.bind(pid)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
|
||||
} else {
|
||||
let mut tx = pool.begin().await.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO certificates (hash, banned)
|
||||
VALUES ($1, false)"
|
||||
)
|
||||
.bind(&hash[..])
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO certificate_pids (cert_hash, pid)
|
||||
VALUES ($1, $2)"
|
||||
)
|
||||
.bind(&hash[..])
|
||||
.bind(pid)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
|
||||
tx.commit().await.map_err(|_| INVALID_TOKEN_ERRORS)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
|
|
@ -273,7 +345,7 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
}
|
||||
|
||||
if USE_CERT {
|
||||
let cert = request_try!(
|
||||
let cert_header = request_try!(
|
||||
request
|
||||
.headers()
|
||||
.get("X-Nintendo-Device-Cert")
|
||||
|
|
@ -281,9 +353,13 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
.ok_or(INVALID_TOKEN_ERRORS)
|
||||
);
|
||||
|
||||
let Some(cert) = Certificate::new(&cert) else {
|
||||
let Some(cert) = Certificate::new(&cert_header) else {
|
||||
return Outcome::Error((Status::BadGateway, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
|
||||
if let Err(_) = handle_certificate(pool, &cert, user.pid).await {
|
||||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
}
|
||||
}
|
||||
|
||||
// let user = User{
|
||||
|
|
@ -311,7 +387,7 @@ pub struct Certificate {
|
|||
#[br(magic(0x10005u32))]
|
||||
struct OuterCertificate {
|
||||
signature: [u8; 0x3C],
|
||||
padding: [u8; 0x40],
|
||||
_padding: [u8; 0x40],
|
||||
data: [u8; 0x100],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use juniper::{graphql_object, EmptyMutation, EmptySubscription, GraphQLObject, RootNode};
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::State;
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use std::env;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use rocket::{get, State};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
|
@ -8,7 +8,7 @@ use crate::nnid::oauth::generate_token::token_type::AUTH_TOKEN;
|
|||
use crate::Pool;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TokenData{
|
||||
pub struct TokenData{
|
||||
token: String,
|
||||
expiry: NaiveDateTime
|
||||
}
|
||||
|
|
@ -17,8 +17,6 @@ struct TokenData{
|
|||
pub async fn generate_token(pool: &State<Pool>, auth: Auth<false>) -> Json<TokenData>{
|
||||
let pool = pool.inner();
|
||||
|
||||
|
||||
|
||||
Json(
|
||||
TokenData{
|
||||
expiry: Utc::now().naive_utc() + Duration::hours(1),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use rocket::{get, State};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::de::IntoDeserializer;
|
||||
// use serde::de::IntoDeserializer;
|
||||
use sqlx::query;
|
||||
use crate::account::account::Auth;
|
||||
use crate::nnid::people::{build_profile, GetOwnProfileData};
|
||||
// use crate::account::account::Auth;
|
||||
// use crate::nnid::people::{build_profile, GetOwnProfileData};
|
||||
use crate::Pool;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ use crate::nnid::people::{build_profile, GetOwnProfileData};
|
|||
use crate::Pool;
|
||||
|
||||
#[get("/api/v2/users/@me/profile")]
|
||||
pub async fn get_own_profile(pool: &State<Pool>, auth: Auth<true>) -> Json<GetOwnProfileData> {
|
||||
pub async fn get_own_profile(_pool: &State<Pool>, auth: Auth<true>) -> Json<GetOwnProfileData> {
|
||||
Json(build_profile(auth.into()))
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
use std::env;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use dotenvy::dotenv;
|
||||
use juniper::{EmptyMutation, EmptySubscription};
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::http::{ContentType, Header, Method, Status};
|
||||
use rocket::{catch, catchers, routes, Request};
|
||||
|
|
@ -26,7 +24,6 @@ mod data_wrapper;
|
|||
mod grpc;
|
||||
mod graphql;
|
||||
mod email;
|
||||
mod papi;
|
||||
mod mii_util;
|
||||
mod json_api;
|
||||
|
||||
|
|
@ -116,7 +113,7 @@ async fn launch() -> _ {
|
|||
EmptySubscription::new())
|
||||
)
|
||||
.attach(AdHoc::on_response("org", |_, response| Box::pin(async move {
|
||||
response.adjoin_header(Header::new("x-organization", "Nintendo"));
|
||||
response.adjoin_header(Header::new("X-Organization", "Nintendo"));
|
||||
response.adjoin_header(Header::new("X-Nintendo-Date", SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
|
|
@ -155,8 +152,6 @@ async fn launch() -> _ {
|
|||
json_api::users::profile::get_own_profile,
|
||||
json_api::users::mii::get_mii_data_by_pid,
|
||||
json_api::users::delete::delete_account,
|
||||
papi::login::login,
|
||||
papi::user::get_user,
|
||||
nnid::people::thing,
|
||||
// graphql::graphiql,
|
||||
// graphql::playground,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::{env, io};
|
||||
use std::collections::HashSet;
|
||||
use gxhash::HashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::{get, Request};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::io::Cursor;
|
|||
|
||||
use rocket::{Request, response::{Responder, Response}};
|
||||
use rocket::http::Header;
|
||||
use time::{OffsetDateTime, Time};
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc2822;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use rocket::{post, FromForm, State};
|
||||
use rocket::form::Form;
|
||||
use serde::{Serialize};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gxhash::{gxhash32, gxhash64};
|
||||
use rocket::{get, post, put, State};
|
||||
|
|
@ -73,8 +74,8 @@ pub struct Email{
|
|||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateMiiData {
|
||||
name: Box<str>,
|
||||
primary: crate::xml::YesNoVal,
|
||||
_name: Box<str>,
|
||||
_primary: crate::xml::YesNoVal,
|
||||
data: Box<str>,
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ pub struct AccountCreationResponseData{
|
|||
}
|
||||
|
||||
#[post("/v1/api/people", data="<data>")]
|
||||
pub async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Option<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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::Pool;
|
||||
use crate::error::{Error, Errors};
|
||||
use chrono::Utc;
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use rocket::form::Form;
|
||||
use rocket::{FromForm, State, post, put};
|
||||
|
||||
|
|
@ -15,8 +16,58 @@ const BAD_CODE_ERROR: Errors = Errors {
|
|||
pub struct ValidateEmailInput {
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[post("/v1/api/support/validate/email", data = "<data>")]
|
||||
pub async fn validate(data: Form<ValidateEmailInput>) {}
|
||||
pub async fn validate(
|
||||
data: Form<ValidateEmailInput>,
|
||||
) -> Result<(), Errors<'static>> {
|
||||
|
||||
let email = data.email.trim();
|
||||
|
||||
// 1. Validate presence + basic format
|
||||
if email.is_empty() || !email.contains('@') {
|
||||
return Err(Errors {
|
||||
error: &[Error {
|
||||
code: "0103",
|
||||
message: "Email format is invalid",
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Extract domain safely
|
||||
let domain = match email.split('@').nth(1) {
|
||||
Some(d) if !d.is_empty() => d,
|
||||
_ => {
|
||||
return Err(Errors {
|
||||
error: &[Error {
|
||||
code: "0103",
|
||||
message: "Email format is invalid",
|
||||
}],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 3. DNS resolver
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf()
|
||||
.map_err(|_| Errors {
|
||||
error: &[Error {
|
||||
code: "1126",
|
||||
message: "DNS resolver initialization failed",
|
||||
}],
|
||||
})?;
|
||||
|
||||
// 4. MX lookup
|
||||
match resolver.mx_lookup(domain).await {
|
||||
Ok(mx) if mx.iter().next().is_some() => Ok(()),
|
||||
|
||||
_ => Err(Errors {
|
||||
error: &[Error {
|
||||
code: "1126",
|
||||
message: "The domain is not accessible",
|
||||
}],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/v1/api/support/email_confirmation/<pid>/<code>")]
|
||||
pub async fn verify_email(
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
use rocket::{post, State};
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use crate::Pool;
|
||||
use crate::account::account::{User, read_bearer_auth_token};
|
||||
use crate::nnid::oauth::generate_token::{create_token, token_type::AUTH_TOKEN, token_type::AUTH_REFRESH_TOKEN};
|
||||
use crate::error::{Error, Errors};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
grant_type: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResponse {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
expires_in: i32,
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
const INVALID_GRANT_TYPE_ERROR: Errors<'static> = Errors {
|
||||
error: &[Error {
|
||||
code: "0100",
|
||||
message: "Invalid grant type",
|
||||
}]
|
||||
};
|
||||
|
||||
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors<'static> = Errors {
|
||||
error: &[Error {
|
||||
code: "0106",
|
||||
message: "Invalid account ID or password",
|
||||
}]
|
||||
};
|
||||
|
||||
const INVALID_REFRESH_TOKEN_ERRORS: Errors<'static> = Errors {
|
||||
error: &[Error {
|
||||
code: "0107",
|
||||
message: "Invalid or missing refresh token",
|
||||
}]
|
||||
};
|
||||
|
||||
#[post("/v1/login", data = "<form_data>")]
|
||||
pub async fn login(pool: &State<Pool>, form_data: Json<LoginRequest>) -> Result<Json<LoginResponse>, Option<Errors<'static>>> {
|
||||
let pool = pool.inner();
|
||||
let grant_type = form_data.grant_type.as_str();
|
||||
|
||||
if grant_type != "password" && grant_type != "refresh_token" {
|
||||
return Err(Some(INVALID_GRANT_TYPE_ERROR));
|
||||
}
|
||||
|
||||
let user: User;
|
||||
|
||||
if grant_type == "password" {
|
||||
let username = form_data.username.as_ref().ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||
let password = form_data.password.as_ref().ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||
|
||||
user = User::get_by_username(username, pool)
|
||||
.await
|
||||
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||
|
||||
if !user.verify_cleartext_password(password).is_some_and(|v| v) {
|
||||
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||
}
|
||||
} else {
|
||||
let refresh_token = form_data.refresh_token.as_ref().ok_or(Some(INVALID_REFRESH_TOKEN_ERRORS))?;
|
||||
|
||||
user = read_bearer_auth_token(pool, refresh_token)
|
||||
.await
|
||||
.ok_or(Some(INVALID_REFRESH_TOKEN_ERRORS))?;
|
||||
}
|
||||
|
||||
if user.account_level < 0 {
|
||||
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||
}
|
||||
|
||||
let access_token = create_token(pool, user.pid, AUTH_TOKEN, None).await;
|
||||
let refresh_token = create_token(pool, user.pid, AUTH_REFRESH_TOKEN, None).await;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
access_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: 3600,
|
||||
refresh_token,
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#[deprecated(note="please use the upcoming api instead of this")]
|
||||
pub mod user;
|
||||
#[deprecated(note="please use the upcoming api instead of this")]
|
||||
pub mod login;
|
||||
113
src/papi/user.rs
113
src/papi/user.rs
|
|
@ -1,113 +0,0 @@
|
|||
use std::env;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::{get};
|
||||
use crate::account::account::{Auth};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
pub static CDN_URL: Lazy<Box<str>> = Lazy::new(||
|
||||
env::var("CDN_URL").expect("CDN_URL not specified").into_boxed_str()
|
||||
);
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct EmailInfo {
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct TimezoneInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct MiiInfo {
|
||||
data: String,
|
||||
name: String,
|
||||
image_url: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct FlagsInfo {
|
||||
marketing: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ConnectionsInfo {
|
||||
discord: DiscordInfo,
|
||||
stripe: StripeInfo,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct DiscordInfo {
|
||||
id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct StripeInfo {
|
||||
tier_name: Option<String>,
|
||||
tier_level: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct UserInfoResponse {
|
||||
deleted: bool,
|
||||
access_level: i32,
|
||||
server_access_level: String,
|
||||
pid: i32,
|
||||
creation_date: chrono::NaiveDateTime,
|
||||
updated: chrono::NaiveDateTime,
|
||||
username: String,
|
||||
birthdate: chrono::NaiveDate,
|
||||
gender: String,
|
||||
country: String,
|
||||
email: EmailInfo,
|
||||
timezone: TimezoneInfo,
|
||||
mii: MiiInfo,
|
||||
flags: FlagsInfo,
|
||||
connections: ConnectionsInfo,
|
||||
}
|
||||
|
||||
#[get("/v1/user")]
|
||||
pub async fn get_user(auth: Auth<false>) -> Json<UserInfoResponse> {
|
||||
let user = auth.0;
|
||||
|
||||
Json(UserInfoResponse {
|
||||
deleted: false,
|
||||
access_level: user.account_level,
|
||||
server_access_level: "test".to_string(),
|
||||
pid: user.pid,
|
||||
creation_date: user.creation_date,
|
||||
updated: user.updated,
|
||||
username: user.username.clone(),
|
||||
birthdate: user.birthdate,
|
||||
gender: user.gender.clone(),
|
||||
country: user.country.clone(),
|
||||
email: EmailInfo {
|
||||
address: user.email.clone(),
|
||||
},
|
||||
timezone: TimezoneInfo {
|
||||
name: user.timezone.clone(),
|
||||
},
|
||||
mii: MiiInfo {
|
||||
data: user.mii_data.clone(),
|
||||
name: {
|
||||
let cleaned = user.mii_data.replace('\n', "").replace('\r', "");
|
||||
mii::MiiData::read(&cleaned)
|
||||
.map(|v| v.name)
|
||||
.unwrap_or_else(|| "INVALID".to_string())
|
||||
},
|
||||
image_url: format!("https://{}/mii/{}/normal_face.png", &CDN_URL.to_string(), user.pid),
|
||||
},
|
||||
flags: FlagsInfo {
|
||||
marketing: user.marketing_allowed,
|
||||
},
|
||||
connections: ConnectionsInfo {
|
||||
discord: DiscordInfo {
|
||||
id: None,
|
||||
},
|
||||
stripe: StripeInfo {
|
||||
tier_name: None,
|
||||
tier_level: None,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue