Compare commits

...

2 commits

Author SHA1 Message Date
893bcecc9e Remove gRPC
All checks were successful
Build and Test / account (push) Successful in 6m16s
RNEX is already in a working state
2026-04-27 18:26:24 +02:00
c0fdc1445d Redo cert request guard 2026-04-27 18:24:37 +02:00
8 changed files with 68 additions and 224 deletions

View file

@ -19,6 +19,3 @@ S3_BUCKET=account-rs
# Make sure to put a secure AES key here as this encrypts all tokens. # Make sure to put a secure AES key here as this encrypts all tokens.
ACCOUNT_AES_KEY=abcdef0123456789abcdef0123456789 ACCOUNT_AES_KEY=abcdef0123456789abcdef0123456789
# You'll only be using gRPC if you're using Pretendo code but it's still recommended to set something secure here.
GRPC_PASSWORD=123456

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "grpc-protobufs"]
path = grpc-protobufs
url = https://github.com/PretendoNetwork/grpc-protobufs.git

View file

@ -1,11 +0,0 @@
fn main(){
tonic_build::configure()
.build_server(true)
.build_client(false)
.compile_protos(
&["grpc-protobufs/account/account_service.proto"],
&["grpc-protobufs/account"]
)
.unwrap();
}

@ -1 +0,0 @@
Subproject commit 410111190ec9f540d60108b70d55a437a6caf68e

View file

@ -63,7 +63,7 @@ pub struct User {
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
pub struct CertificateRecord { pub struct CertificateRecord {
pub _hash: Vec<u8>, pub hash: Vec<u8>,
pub banned: bool, pub banned: bool,
} }
@ -252,26 +252,13 @@ impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> Into<User>
} }
} }
pub async fn handle_certificate( pub async fn link_certificate_to_pid(
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
cert: &Certificate, cert: &Certificate,
pid: i32, pid: i32,
) -> Result<(), Errors<'static>> { ) -> Result<(), Errors<'static>> {
let hash = cert.hash(); 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( sqlx::query(
"INSERT INTO certificate_pids (cert_hash, pid) "INSERT INTO certificate_pids (cert_hash, pid)
VALUES ($1, $2) VALUES ($1, $2)
@ -283,31 +270,6 @@ pub async fn handle_certificate(
.await .await
.map_err(|_| INVALID_TOKEN_ERRORS)?; .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(()) Ok(())
} }
@ -344,24 +306,6 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)); return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
} }
if USE_CERT {
let cert_header = request_try!(
request
.headers()
.get("X-Nintendo-Device-Cert")
.next()
.ok_or(INVALID_TOKEN_ERRORS)
);
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{ // let user = User{
// nex_password: format!("{:a>16}", user.nex_password), // nex_password: format!("{:a>16}", user.nex_password),
// ..user // ..user
@ -371,6 +315,62 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
} }
} }
pub struct DeviceCert(pub Certificate);
#[async_trait]
impl<'r> FromRequest<'r> for DeviceCert {
type Error = Errors<'static>;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let pool: &sqlx::PgPool = request.rocket().state().unwrap();
let cert_header = match request.headers().get("X-Nintendo-Device-Cert").next() {
Some(h) => h,
None => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
};
let cert = match Certificate::new(cert_header) {
Some(c) => c,
None => return Outcome::Error((Status::BadGateway, INVALID_TOKEN_ERRORS)),
};
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);
let existing = match existing {
Ok(v) => v,
Err(e) => {
println!("certificate query failed: {:?}", e);
return Outcome::Error((Status::InternalServerError, INVALID_TOKEN_ERRORS));
}
};
if let Some(row) = existing {
if row.banned {
return Outcome::Error((Status::Forbidden, INVALID_TOKEN_ERRORS));
}
} else {
sqlx::query(
"INSERT INTO certificates (hash, banned)
VALUES ($1, false)"
)
.bind(&hash[..])
.execute(pool)
.await
.map_err(|_| INVALID_TOKEN_ERRORS);
}
Outcome::Success(DeviceCert(cert))
}
}
#[binread] #[binread]
#[br(big)] #[br(big)]
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,105 +0,0 @@
use crate::Pool;
use crate::grpc::grpc::{
ExchangeTokenForUserDataRequest, GetNexDataRequest, GetNexDataResponse, GetNexPasswordRequest,
GetNexPasswordResponse, GetUserDataRequest, GetUserDataResponse, UpdatePnidPermissionsRequest,
};
use once_cell::sync::Lazy;
use std::env;
use tonic::metadata::MetadataMap;
use tonic::{Request, Response, Status, async_trait};
/// This module is a legacy module meant for interacting with existing pretendo
/// servers. This will inevitably be removed completely as this is only meant as
/// a stopgap until RNEX is in a fully functional state.
pub mod grpc {
tonic::include_proto!("account");
}
static GRPC_PASSWORD: Lazy<Box<str>> = Lazy::new(|| {
env::var("GRPC_PASSWORD")
.expect("GRPC_PASSWORD not specified")
.into_boxed_str()
});
fn verify_grpc_key(meta: &MetadataMap) -> Result<(), Status> {
// req.metadata_mut().insert("x-api-key", API_KEY.clone());
let key = meta
.get("x-api-key")
.ok_or(Status::permission_denied("api key missing"))?;
if key.as_bytes() != GRPC_PASSWORD.as_bytes() {
return Err(Status::permission_denied("GO AWAY"));
}
Ok(())
}
pub struct AccountService(pub Pool);
#[async_trait]
impl grpc::account_server::Account for AccountService {
async fn exchange_token_for_user_data(
&self,
request: Request<ExchangeTokenForUserDataRequest>,
) -> Result<Response<GetUserDataResponse>, Status> {
verify_grpc_key(request.metadata())?;
Err(Status::unimplemented(
"grpc tecnically isnt supported by account-rs as such no full support is guaranteed(you called a stubbed function)",
))
}
async fn get_nex_data(
&self,
request: Request<GetNexDataRequest>,
) -> Result<Response<GetNexDataResponse>, Status> {
verify_grpc_key(request.metadata())?;
Err(Status::unimplemented(
"grpc tecnically isnt supported by account-rs as such no full support is guaranteed(you called a stubbed function)",
))
}
async fn get_nex_password(
&self,
request: Request<GetNexPasswordRequest>,
) -> Result<Response<GetNexPasswordResponse>, Status> {
verify_grpc_key(request.metadata())?;
let data = request.get_ref();
let password = sqlx::query!(
"select nex_password from users where pid = $1",
data.pid as i32
)
.fetch_one(&self.0)
.await
.map_err(|_| Status::invalid_argument("No NEX account found"))?
.nex_password;
// let password_padded = format!("{:a>16}", password);
Ok(Response::new(GetNexPasswordResponse { password }))
}
async fn update_pnid_permissions(
&self,
request: Request<UpdatePnidPermissionsRequest>,
) -> Result<Response<()>, Status> {
verify_grpc_key(request.metadata())?;
Err(Status::unimplemented(
"grpc tecnically isnt supported by account-rs as such no full support is guaranteed(you called a stubbed function)",
))
}
async fn get_user_data(
&self,
request: Request<GetUserDataRequest>,
) -> Result<Response<GetUserDataResponse>, Status> {
verify_grpc_key(request.metadata())?;
Err(Status::unimplemented(
"grpc tecnically isnt supported by account-rs as such no full support is guaranteed(you called a stubbed function)",
))
}
}

View file

@ -20,8 +20,6 @@ mod account;
mod error; mod error;
mod dsresponse; mod dsresponse;
mod data_wrapper; mod data_wrapper;
// #[deprecated]
mod grpc;
mod graphql; mod graphql;
mod email; mod email;
mod mii_util; mod mii_util;
@ -29,37 +27,6 @@ mod json_api;
type Pool = sqlx::Pool<Postgres>; type Pool = sqlx::Pool<Postgres>;
async fn start_grpc(){
let act_database_url = env::var("DATABASE_URL").expect("account database url is not set");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&act_database_url)
.await
.expect("unable to create pool");
let grpc_instance = grpc::AccountService(pool);
let addr: SocketAddr =
SocketAddr::from((
env::var("ROCKET_ADDRESS").ok()
.map(|v| v.parse().expect("unable to read address"))
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
7071
)
);
tokio::spawn(async move{
Server::builder()
.add_service(grpc::grpc::account_server::AccountServer::new(grpc_instance))
.serve(addr)
.await
.expect("unable to start grpc server");
});
}
#[catch(404)] #[catch(404)]
fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) { fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) {
@ -85,8 +52,6 @@ fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) {
async fn launch() -> _ { async fn launch() -> _ {
dotenv().ok(); dotenv().ok();
start_grpc().await;
let act_database_url = env::var("DATABASE_URL").expect("account database url is not set"); let act_database_url = env::var("DATABASE_URL").expect("account database url is not set");
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()

View file

@ -2,7 +2,7 @@
use rocket::{post, FromForm, State}; use rocket::{post, FromForm, State};
use rocket::form::Form; use rocket::form::Form;
use serde::{Serialize}; use serde::{Serialize};
use crate::account::account::User; use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid};
use crate::error::{Error, Errors}; use crate::error::{Error, Errors};
use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING}; use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING};
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN}; use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
@ -101,7 +101,7 @@ pub struct TokenRequestReturnData{
} }
#[post("/v1/api/oauth20/access_token/generate", data="<data>")] #[post("/v1/api/oauth20/access_token/generate", data="<data>")]
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{ 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(); let pool = pool.inner();
let user = User::get_by_username(data.user_id, pool).await let user = User::get_by_username(data.user_id, pool).await
@ -123,6 +123,8 @@ pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>
return Err(Some(ACCOUNT_BANNED_ERRORS)); return Err(Some(ACCOUNT_BANNED_ERRORS));
} }
link_certificate_to_pid(&pool, &cert.0, user.pid).await?;
let access_token = TokenReturnData::new(user.pid, pool).await; let access_token = TokenReturnData::new(user.pid, pool).await;
Ok(Xml(TokenRequestReturnData{ Ok(Xml(TokenRequestReturnData{