Compare commits
No commits in common. "893bcecc9e3ab37c13d7054b56142c8eb5aaaa0b" and "9456013dc7adcb1dcd924b1e02e101084aec5187" have entirely different histories.
893bcecc9e
...
9456013dc7
8 changed files with 224 additions and 68 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "grpc-protobufs"]
|
||||
path = grpc-protobufs
|
||||
url = https://github.com/PretendoNetwork/grpc-protobufs.git
|
||||
11
build.rs
Normal file
11
build.rs
Normal file
|
|
@ -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();
|
||||
}
|
||||
1
grpc-protobufs
Submodule
1
grpc-protobufs
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 410111190ec9f540d60108b70d55a437a6caf68e
|
||||
|
|
@ -63,7 +63,7 @@ pub struct User {
|
|||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct CertificateRecord {
|
||||
pub hash: Vec<u8>,
|
||||
pub _hash: Vec<u8>,
|
||||
pub banned: bool,
|
||||
}
|
||||
|
||||
|
|
@ -252,13 +252,26 @@ impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> Into<User>
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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)
|
||||
|
|
@ -270,6 +283,31 @@ pub async fn link_certificate_to_pid(
|
|||
.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<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]
|
||||
#[br(big)]
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
105
src/grpc/mod.rs
Normal file
105
src/grpc/mod.rs
Normal file
|
|
@ -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<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)",
|
||||
))
|
||||
}
|
||||
}
|
||||
35
src/main.rs
35
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<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)]
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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="<data>")]
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP, cert: DeviceCert) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
|
||||
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<Pool>, data: Form<TokenRequestData<'_>>
|
|||
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{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue