From 2417c109e44ceb7bd840c0641123da5c5f524825 Mon Sep 17 00:00:00 2001 From: DJMrTV Date: Sun, 9 Mar 2025 23:47:46 +0100 Subject: [PATCH] feat: get in the friends server --- Cargo.lock | 89 ++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++ src/account/account.rs | 2 +- src/graphql/mod.rs | 107 +++++++++++++++++++++++++++++++++++++++++ src/grpc/mod.rs | 103 +++++++++++++++++++++++++++++++++------ src/main.rs | 59 ++++++++++++++++++++++- src/nnid/mapped_ids.rs | 83 ++++++++++++++++++++++++++++++++ src/nnid/mod.rs | 1 + src/nnid/provider.rs | 48 ++++++++++++++++-- 9 files changed, 474 insertions(+), 22 deletions(-) create mode 100644 src/graphql/mod.rs create mode 100644 src/nnid/mapped_ids.rs diff --git a/Cargo.lock b/Cargo.lock index c027ea7..9f3a89b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b43b857..59de8b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/src/account/account.rs b/src/account/account.rs index a8142ab..6ceead2 100644 --- a/src/account/account.rs +++ b/src/account/account.rs @@ -124,7 +124,7 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option Option { +pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option { let data = TokenData::decode(token)?; let token_info = diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs new file mode 100644 index 0000000..8f100b4 --- /dev/null +++ b/src/graphql/mod.rs @@ -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, + EmptySubscription +>; + + +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 +} + +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{ + 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 { + juniper_rocket::graphiql_source("/graphql", None) +} + + +#[rocket::get("/playground")] +pub fn playground() -> RawHtml { + juniper_rocket::playground_source("/graphql", None) +} + +#[rocket::get("/graphql?")] +pub async fn get_graphql( + db: &State, + request: juniper_rocket::GraphQLRequest, + schema: &State, +) -> juniper_rocket::GraphQLResponse { + request.execute(schema, db).await +} + +#[rocket::post("/graphql", data = "")] +pub async fn post_graphql( + db: &State, + request: juniper_rocket::GraphQLRequest, + schema: &State, +) -> juniper_rocket::GraphQLResponse { + request.execute(schema, db).await +} \ No newline at end of file diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 9c6304e..80c4461 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -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> = 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> { - todo!() +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> { - todo!() + 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> { - todo!() + 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; + + Ok(Response::new(GetNexPasswordResponse { password })) } - async fn update_pnid_permissions(&self, request: Request) -> Result, Status> { - todo!() + 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> { - todo!() + 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)", + )) } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index a4d3560..26a766a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; +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, ]) } diff --git a/src/nnid/mapped_ids.rs b/src/nnid/mapped_ids.rs new file mode 100644 index 0000000..c6bbc75 --- /dev/null +++ b/src/nnid/mapped_ids.rs @@ -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, +} + +#[derive(Serialize)] +#[serde(rename = "mapped_ids")] +struct MappedIds { + mapped_id: Vec, +} + +struct UserIdAndName { + pid: i32, + username: String, +} + +#[get("/v1/api/admin/mapped_ids?&&")] +pub async fn mapped_ids(pool: &State, input_type: String, output_type: String, input: String) -> Option> { + 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 + } + )) +} \ No newline at end of file diff --git a/src/nnid/mod.rs b/src/nnid/mod.rs index 3fa13c1..e0c1f2f 100644 --- a/src/nnid/mod.rs +++ b/src/nnid/mod.rs @@ -7,3 +7,4 @@ pub mod oauth; mod pid_distribution; pub mod people; pub mod provider; +pub mod mapped_ids; diff --git a/src/nnid/provider.rs b/src/nnid/provider.rs index e24d7e5..fc33ac0 100644 --- a/src/nnid/provider.rs +++ b/src/nnid/provider.rs @@ -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?")] -pub async fn get_nex_token(pool: &State, auth: Auth, game_server_id: String) -> Option>{ +#[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, auth: Auth) -> Option>{ // 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?")] +pub async fn get_nex_token(pool: &State, auth: Auth, game_server_id: &str) -> Option>{ + // 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;