feat: get in the friends server

This commit is contained in:
DJMrTV 2025-03-09 23:47:46 +01:00
commit 2417c109e4
9 changed files with 473 additions and 21 deletions

89
Cargo.lock generated
View file

@ -18,6 +18,8 @@ dependencies = [
"gxhash",
"hex",
"hmac",
"juniper",
"juniper_rocket",
"log",
"md-5",
"mii",
@ -234,6 +236,18 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto_enums"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c170965892137a3a9aeb000b4524aa3cc022a310e709d848b6e1cdce4ab4781"
dependencies = [
"derive_utils",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -617,6 +631,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_utils"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "devise"
version = "0.4.2"
@ -839,6 +864,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -1558,6 +1584,52 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "juniper"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943306315b1a7a03d27af9dfb0c288d9f4da8830c17df4bceb7d50a47da0982c"
dependencies = [
"async-trait",
"auto_enums",
"chrono",
"fnv",
"futures",
"indexmap 2.7.1",
"juniper_codegen",
"serde",
"smartstring",
"static_assertions",
]
[[package]]
name = "juniper_codegen"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760dbe46660494d469023d661e8d268f413b2cb68c999975dcc237407096a693"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
"url",
]
[[package]]
name = "juniper_rocket"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce74561b72a9aab16a16df022d7b6551fa8018b0cedfe2187417eeb602b65b3e"
dependencies = [
"either",
"futures",
"inlinable_string",
"juniper",
"pear",
"rocket",
"serde_json",
"tempfile",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -2708,6 +2780,17 @@ dependencies = [
"serde",
]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.5.8"
@ -2955,6 +3038,12 @@ dependencies = [
"loom",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stringprep"
version = "0.1.5"

View file

@ -39,9 +39,13 @@ minio = { git = "https://github.com/minio/minio-rs.git" }
crc32fast = "1.4.2"
gxhash = "3.4.1"
juniper = { version = "0.16.1", features = ["chrono"] }
juniper_rocket = "0.9.0"
tonic = "0.12.3"
prost = "0.13.4"
[build-dependencies]
tonic-build = "0.12.3"

View file

@ -124,7 +124,7 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
}
}
async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
let data = TokenData::decode(token)?;
let token_info =

107
src/graphql/mod.rs Normal file
View file

@ -0,0 +1,107 @@
use std::fmt::Display;
use chrono::NaiveDateTime;
use juniper::{graphql_object, EmptyMutation, EmptySubscription, GraphQLObject, RootNode, ScalarValue};
use rocket::response::content::RawHtml;
use rocket::State;
use crate::account::account::{read_basic_auth_token, read_bearer_auth_token};
use crate::nnid::oauth::TokenData;
use crate::Pool;
pub type Schema = RootNode<
'static,
Query,
EmptyMutation<Context>,
EmptySubscription<Context>
>;
pub struct Context(pub Pool);
impl juniper::Context for Context{}
#[derive(GraphQLObject)]
#[graphql(description = "Data inside of a token")]
struct TokenInfo {
pid: i32,
expire_date: NaiveDateTime,
title_id: Option<String>
}
pub struct Query;
#[graphql_object]
#[graphql(context = Context)]
impl Query {
fn api_version() -> &'static str {
"1.0"
}
async fn token(
token_data: String,
context: &Context,
) -> Option<TokenInfo>{
let data = TokenData::decode(&token_data)?;
let token_info =
sqlx::query!(
"select * from tokens where pid = $1 and token_id = $2 and random = $3",
data.pid, data.token_id, data.random
).
fetch_one(&context.0).await.ok()?;
Some(TokenInfo{
pid: data.pid,
expire_date: token_info.expires,
title_id: token_info.title_id,
})
}
}
/*
struct Mutation;
#[graphql_object]
#[graphql(
context = Context,
// If we need to use `ScalarValue` parametrization explicitly somewhere
// in the object definition (like here in `FieldResult`), we could
// declare an explicit type parameter for that, and specify it.
scalar = S: ScalarValue + Display,
)]
impl Mutation {
}
*/
#[rocket::get("/graphiql")]
pub fn graphiql() -> RawHtml<String> {
juniper_rocket::graphiql_source("/graphql", None)
}
#[rocket::get("/playground")]
pub fn playground() -> RawHtml<String> {
juniper_rocket::playground_source("/graphql", None)
}
#[rocket::get("/graphql?<request..>")]
pub async fn get_graphql(
db: &State<Context>,
request: juniper_rocket::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(schema, db).await
}
#[rocket::post("/graphql", data = "<request>")]
pub async fn post_graphql(
db: &State<Context>,
request: juniper_rocket::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(schema, db).await
}

View file

@ -1,30 +1,103 @@
use tonic::{async_trait, Request, Response, Status};
use crate::grpc::grpc::{ExchangeTokenForUserDataRequest, GetNexDataRequest, GetNexDataResponse, GetNexPasswordRequest, GetNexPasswordResponse, GetUserDataRequest, GetUserDataResponse, UpdatePnidPermissionsRequest};
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};
mod grpc {
/// 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> {
todo!()
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> {
todo!()
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> {
todo!()
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;
Ok(Response::new(GetNexPasswordResponse { password }))
}
async fn update_pnid_permissions(&self, request: Request<UpdatePnidPermissionsRequest>) -> Result<Response<()>, Status> {
todo!()
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> {
todo!()
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

@ -1,15 +1,20 @@
use std::env;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use dotenvy::dotenv;
use juniper::{EmptyMutation, EmptySubscription};
use log::info;
use rocket::fairing::AdHoc;
use rocket::futures::FutureExt;
use rocket::http::Header;
use rocket::routes;
use sqlx::Postgres;
use sqlx::postgres::PgPoolOptions;
use tonic::transport::Server;
use crate::graphql::{Query, Schema};
mod xml;
mod conntest;
@ -18,15 +23,49 @@ mod account;
mod error;
mod dsresponse;
mod data_wrapper;
#[deprecated]
mod grpc;
mod graphql;
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");
});
}
#[rocket::launch]
async fn launch() -> _ {
dotenv().ok();
start_grpc().await;
let act_database_url = env::var("DATABASE_URL").expect("account database url is not set");
@ -35,8 +74,19 @@ async fn launch() -> _ {
.connect(&act_database_url).await
.expect("unable to create pool");
let graph_pool = PgPoolOptions::new()
.max_connections(5)
.connect(&act_database_url).await
.expect("unable to create pool");
rocket::build()
.manage(pool)
.manage(graphql::Context(graph_pool))
.manage(Schema::new(
Query,
EmptyMutation::new(),
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-nintendo-date", SystemTime::now()
@ -64,5 +114,12 @@ async fn launch() -> _ {
nnid::people::get_own_profile,
nnid::oauth::generate_token::generate_token,
nnid::provider::get_nex_token,
nnid::provider::get_service_token,
nnid::mapped_ids::mapped_ids,
//graphql
graphql::graphiql,
graphql::playground,
graphql::get_graphql,
graphql::post_graphql,
])
}

83
src/nnid/mapped_ids.rs Normal file
View file

@ -0,0 +1,83 @@
use rocket::{get, State};
use serde::Serialize;
use crate::Pool;
use crate::xml::Xml;
#[derive(Serialize)]
#[serde(rename = "mapped_id")]
struct MappedId {
in_id: String,
out_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename = "mapped_ids")]
struct MappedIds {
mapped_id: Vec<MappedId>,
}
struct UserIdAndName {
pid: i32,
username: String,
}
#[get("/v1/api/admin/mapped_ids?<input_type>&<output_type>&<input>")]
pub async fn mapped_ids(pool: &State<Pool>, input_type: String, output_type: String, input: String) -> Option<Xml<MappedIds>> {
let pool = pool.inner();
let is_input_pid = input_type == "pid";
let is_output_pid = output_type == "pid";
let mut outputs = Vec::new();
for input in input.split(',') {
if input == ""{
continue;
}
let Some(user) =
(if is_input_pid {
let id: i32 = input.parse().ok()?;
sqlx::query_as!(
UserIdAndName,
"select pid, username from users where pid = $1",
id
).fetch_one(pool)
.await.ok()
} else {
sqlx::query_as!(
UserIdAndName,
"select pid, username from users where username = $1",
input
).fetch_one(pool)
.await.ok()
}) else {
outputs.push(MappedId{
in_id: input.to_string(),
out_id: None,
});
continue
};
if is_output_pid{
outputs.push(MappedId{
in_id: input.to_string(),
out_id: Some(user.pid.to_string()),
})
} else {
outputs.push(MappedId{
in_id: input.to_string(),
out_id: Some(user.username),
})
}
}
Some(Xml(
MappedIds{
mapped_id: outputs
}
))
}

View file

@ -7,3 +7,4 @@ pub mod oauth;
mod pid_distribution;
pub mod people;
pub mod provider;
pub mod mapped_ids;

View file

@ -6,12 +6,19 @@ use sqlx::types::ipnetwork::IpNetwork::V4;
use crate::account::account::Auth;
use crate::nnid::oauth::generate_token::create_token;
use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN;
use crate::nnid::provider::Test::{A, B};
use crate::Pool;
use crate::xml::Xml;
enum Test{
A(String),
B(i32)
}
#[derive(Serialize)]
#[serde(rename = "nex_token")]
struct NexToken{
pub struct NexToken{
host: Ipv4Addr,
nex_password: String,
pid: i32,
@ -19,20 +26,51 @@ struct NexToken{
token: String
}
#[get("/v1/api/provider/nex_token/@me?<game_server_id>")]
pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true>, game_server_id: String) -> Option<Xml<NexToken>>{
#[derive(Serialize)]
#[serde(rename = "service_token")]
pub struct ServiceToken{
token: String
}
#[get("/v1/api/provider/service_token/@me")]
pub async fn get_service_token(pool: &State<Pool>, auth: Auth<true>) -> Option<Xml<ServiceToken>>{
// just gonna put this here as a side note for the future:
// we could also be using key derivation to derive the nex token as if it were a key
// that way we could reduce the data the database needs to store and also reduce the transfer
// cost of sending an entire row from the user table (which is required for the auth code unless
// we change the way we read in data to essentially having the user object be a proxy for its
// table row)
let pool = pool.inner();
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
Some(
Xml(
ServiceToken{
token
}
)
)
}
#[get("/v1/api/provider/nex_token/@me?<game_server_id>")]
pub async fn get_nex_token(pool: &State<Pool>, auth: Auth<true>, game_server_id: &str) -> Option<Xml<NexToken>>{
// just gonna put this here as a side note for the future:
// we could also be using key derivation to derive the nex token as if it were a key
// that way we could reduce the data the database needs to store and also reduce the transfer
// cost of sending an entire row from the user table (which is required for the auth code unless
// we change the way we read in data to essentially having the user object be a proxy for its
// table row)
let pool = pool.inner();
let server = sqlx::query!(
"select * from nex_servers where game_server_id = $1",
"select address, port from nex_servers where game_server_id = $1",
game_server_id
) .fetch_one(pool).await.ok()?;
) .fetch_one(pool).await.unwrap();
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;