diff --git a/.env.example b/.env.example index a045f44..3fb0605 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,6 @@ S3_BUCKET=account-rs # Make sure to put a secure AES key here as this encrypts all tokens. 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 + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..64cd88d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "grpc-protobufs"] + path = grpc-protobufs + url = https://github.com/PretendoNetwork/grpc-protobufs.git diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f7a79e0 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ + +fn main(){ + tonic_build::configure() + .build_server(true) + .build_client(false) + .compile_protos( + &["grpc-protobufs/account/account_service.proto"], + &["grpc-protobufs/account"] + ) + .unwrap(); +} \ No newline at end of file diff --git a/grpc-protobufs b/grpc-protobufs new file mode 160000 index 0000000..4101111 --- /dev/null +++ b/grpc-protobufs @@ -0,0 +1 @@ +Subproject commit 410111190ec9f540d60108b70d55a437a6caf68e diff --git a/src/account/account.rs b/src/account/account.rs index 8e81069..12c759a 100644 --- a/src/account/account.rs +++ b/src/account/account.rs @@ -63,7 +63,7 @@ pub struct User { #[derive(sqlx::FromRow)] pub struct CertificateRecord { - pub hash: Vec, + pub _hash: Vec, pub banned: bool, } @@ -252,24 +252,62 @@ impl Into } } -pub async fn link_certificate_to_pid( +pub async fn handle_certificate( pool: &sqlx::PgPool, cert: &Certificate, pid: i32, ) -> Result<(), Errors<'static>> { let hash = cert.hash(); - sqlx::query( - "INSERT INTO certificate_pids (cert_hash, pid) - VALUES ($1, $2) - ON CONFLICT DO NOTHING" + let existing = sqlx::query_as::<_, CertificateRecord>( + "SELECT hash, banned FROM certificates WHERE hash = $1" ) .bind(&hash[..]) - .bind(pid) - .execute(pool) + .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(()) } @@ -306,6 +344,24 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r> 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{ // nex_password: format!("{:a>16}", user.nex_password), // ..user @@ -315,62 +371,6 @@ 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 { - 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] #[br(big)] #[derive(Debug)] diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs new file mode 100644 index 0000000..992fcb9 --- /dev/null +++ b/src/grpc/mod.rs @@ -0,0 +1,105 @@ +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> = 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, + ) -> Result, 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, + ) -> Result, 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, + ) -> Result, 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, + ) -> Result, 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, + ) -> Result, 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)", + )) + } +} diff --git a/src/main.rs b/src/main.rs index e645798..c8e5d99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,8 @@ mod account; mod error; mod dsresponse; mod data_wrapper; +// #[deprecated] +mod grpc; mod graphql; mod email; mod mii_util; @@ -27,6 +29,37 @@ mod json_api; type Pool = sqlx::Pool; +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)] fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) { @@ -52,6 +85,8 @@ fn not_found(_req: &Request) -> (Status, (ContentType, RawXml<&'static str>)) { async fn launch() -> _ { dotenv().ok(); + start_grpc().await; + let act_database_url = env::var("DATABASE_URL").expect("account database url is not set"); let pool = PgPoolOptions::new() diff --git a/src/nnid/oauth/generate_token.rs b/src/nnid/oauth/generate_token.rs index 26c8775..f5de9af 100644 --- a/src/nnid/oauth/generate_token.rs +++ b/src/nnid/oauth/generate_token.rs @@ -2,7 +2,7 @@ use rocket::{post, FromForm, State}; use rocket::form::Form; use serde::{Serialize}; -use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid}; +use crate::account::account::User; use crate::error::{Error, Errors}; use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING}; 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="")] -pub async fn generate_token(pool: &State, data: Form>, ip: CFIP, cert: DeviceCert) -> Result, Option>>{ +pub async fn generate_token(pool: &State, data: Form>, ip: CFIP) -> Result, Option>>{ let pool = pool.inner(); let user = User::get_by_username(data.user_id, pool).await @@ -123,8 +123,6 @@ pub async fn generate_token(pool: &State, data: Form> 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; Ok(Xml(TokenRequestReturnData{