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.
|
# 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
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)]
|
#[derive(sqlx::FromRow)]
|
||||||
pub struct CertificateRecord {
|
pub struct CertificateRecord {
|
||||||
pub hash: Vec<u8>,
|
pub _hash: Vec<u8>,
|
||||||
pub banned: bool,
|
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,
|
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)
|
||||||
|
|
@ -270,6 +283,31 @@ pub async fn link_certificate_to_pid(
|
||||||
.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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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));
|
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
|
||||||
|
|
@ -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]
|
#[binread]
|
||||||
#[br(big)]
|
#[br(big)]
|
||||||
#[derive(Debug)]
|
#[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 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;
|
||||||
|
|
@ -27,6 +29,37 @@ 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>)) {
|
||||||
|
|
@ -52,6 +85,8 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -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::{Auth, DeviceCert, User, link_certificate_to_pid};
|
use crate::account::account::User;
|
||||||
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, 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 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,8 +123,6 @@ 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{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue