diff --git a/Cargo.lock b/Cargo.lock index 39fa9af..cf78903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,10 +1129,10 @@ version = "0.1.0" dependencies = [ "dolphin-integrations", "slippi-game-reporter", + "slippi-gg-api", "slippi-jukebox", "slippi-user", "tracing", - "ureq", ] [[package]] @@ -1145,8 +1145,16 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "slippi-gg-api", "slippi-user", "tracing", +] + +[[package]] +name = "slippi-gg-api" +version = "0.1.0" +dependencies = [ + "tracing", "ureq", ] @@ -1182,8 +1190,8 @@ dependencies = [ "open", "serde", "serde_json", + "slippi-gg-api", "tracing", - "ureq", ] [[package]] @@ -1419,9 +1427,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ "base64", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 9198c24..1beead3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "ffi", "game-reporter", "jukebox", + "slippi-gg-api", "user", ] @@ -32,7 +33,7 @@ serde_repr = { version = "0.1" } # in extra dependencies. tracing = { version = "0.1", default-features = false, features = ["std"] } -ureq = { version = "2.7.1", features = ["json"] } +ureq = { version = "2.9.1", features = ["json"] } [patch.crates-io] # We need to patch this dependency to fix a bug in Windows where a crash can occur diff --git a/dolphin/Cargo.toml b/dolphin/Cargo.toml index f8c8748..ca03dfe 100644 --- a/dolphin/Cargo.toml +++ b/dolphin/Cargo.toml @@ -13,6 +13,7 @@ publish = false default = [] ishiiruka = [] mainline = [] +playback = [] [dependencies] time = { workspace = true } diff --git a/exi/Cargo.toml b/exi/Cargo.toml index 9710da9..6c3a2e6 100644 --- a/exi/Cargo.toml +++ b/exi/Cargo.toml @@ -11,13 +11,20 @@ publish = false [features] default = [] -ishiiruka = [] -mainline = [] +ishiiruka = [ + "slippi-gg-api/ishiiruka" +] +mainline = [ + "slippi-gg-api/mainline" +] +playback = [ + "slippi-gg-api/playback" +] [dependencies] dolphin-integrations = { path = "../dolphin" } slippi-game-reporter = { path = "../game-reporter" } +slippi-gg-api = { path = "../slippi-gg-api" } slippi-jukebox = { path = "../jukebox" } slippi-user = { path = "../user" } tracing = { workspace = true } -ureq = { workspace = true } diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 5f1885c..797a68d 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -5,12 +5,9 @@ //! `SlippiEXIDevice` and forwards calls over the C FFI. This has a fairly clean mapping to "when //! Slippi stuff is happening" and enables us to let the Rust side live in its own world. -use std::time::Duration; - -use ureq::AgentBuilder; - use dolphin_integrations::Log; use slippi_game_reporter::GameReporter; +use slippi_gg_api::APIClient; use slippi_jukebox::Jukebox; use slippi_user::UserManager; @@ -41,22 +38,15 @@ impl SlippiEXIDevice { pub fn new(config: Config) -> Self { tracing::info!(target: Log::SlippiOnline, "Starting SlippiEXIDevice"); - // We set `max_idle_connections` to `5` to mimic how CURL was configured in - // the old C++ logic. This gets cloned and passed down into modules so that - // the underlying connection pool is shared. - let http_client = AgentBuilder::new() - .max_idle_connections(5) - .timeout(Duration::from_millis(5000)) - .user_agent(&format!("SlippiDolphin/{} (Rust)", config.scm.slippi_semver)) - .build(); + let api_client = APIClient::new(&config.scm.slippi_semver); let user_manager = UserManager::new( - http_client.clone(), + api_client.clone(), config.paths.user_json.clone().into(), config.scm.slippi_semver.clone(), ); - let game_reporter = GameReporter::new(http_client.clone(), user_manager.clone(), config.paths.iso.clone()); + let game_reporter = GameReporter::new(api_client.clone(), user_manager.clone(), config.paths.iso.clone()); // Playback has no need to deal with this. // (We could maybe silo more?) diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 5d77a4c..3cd2cca 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -24,8 +24,18 @@ ishiiruka = [ "slippi-exi-device/ishiiruka", "slippi-user/ishiiruka" ] -mainline = [] -playback = [] +mainline = [ + "dolphin-integrations/mainline", + "slippi-game-reporter/mainline", + "slippi-exi-device/mainline", + "slippi-user/mainline" +] +playback = [ + "dolphin-integrations/playback", + "slippi-game-reporter/playback", + "slippi-exi-device/playback", + "slippi-user/playback" +] [dependencies] dolphin-integrations = { path = "../dolphin" } diff --git a/game-reporter/Cargo.toml b/game-reporter/Cargo.toml index 822fb5a..57780db 100644 --- a/game-reporter/Cargo.toml +++ b/game-reporter/Cargo.toml @@ -10,6 +10,7 @@ publish = false default = [] ishiiruka = [] mainline = [] +playback = [] [dependencies] chksum = { version = "0.2.2", default-features = false, features = ["md5"] } @@ -18,6 +19,6 @@ flate2 = "1.0" serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } +slippi-gg-api = { path = "../slippi-gg-api" } slippi-user = { path = "../user" } tracing = { workspace = true } -ureq = { workspace = true } diff --git a/game-reporter/src/lib.rs b/game-reporter/src/lib.rs index bdddf29..31fa61b 100644 --- a/game-reporter/src/lib.rs +++ b/game-reporter/src/lib.rs @@ -7,9 +7,8 @@ use std::sync::Arc; use std::sync::Mutex; use std::thread; -use ureq::Agent; - use dolphin_integrations::Log; +use slippi_gg_api::APIClient; use slippi_user::UserManager; mod iso_md5_hasher; @@ -68,8 +67,8 @@ impl GameReporter { /// /// Currently, failure to spawn any thread should result in a crash - i.e, if we can't /// spawn an OS thread, then there are probably far bigger issues at work here. - pub fn new(http_client: Agent, user_manager: UserManager, iso_path: String) -> Self { - let queue = GameReporterQueue::new(http_client.clone()); + pub fn new(api_client: APIClient, user_manager: UserManager, iso_path: String) -> Self { + let queue = GameReporterQueue::new(api_client.clone()); // This is a thread-safe "one time" setter that the MD5 hasher thread // will set when it's done computing. @@ -97,7 +96,7 @@ impl GameReporter { let completion_thread = thread::Builder::new() .name("GameReporterCompletionProcessingThread".into()) .spawn(move || { - queue::run_completion(http_client, completion_receiver); + queue::run_completion(api_client, completion_receiver); }) .expect("Failed to spawn GameReporterCompletionProcessingThread."); diff --git a/game-reporter/src/queue.rs b/game-reporter/src/queue.rs index d5fde03..08f2a71 100644 --- a/game-reporter/src/queue.rs +++ b/game-reporter/src/queue.rs @@ -1,23 +1,22 @@ //! This module implements the background queue for the Game Reporter. use std::collections::VecDeque; +use std::io::Write; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; +use flate2::write::GzEncoder; +use flate2::Compression; use serde_json::{json, Value}; -use ureq::Agent; use dolphin_integrations::{Color, Dolphin, Duration as OSDDuration, Log}; +use slippi_gg_api::APIClient; use crate::types::{GameReport, GameReportRequestPayload, OnlinePlayMode}; use crate::{CompletionEvent, ProcessingEvent}; -use flate2::write::GzEncoder; -use flate2::Compression; -use std::io::Write; - const GRAPHQL_URL: &str = "https://gql-gateway-dot-slippi.uc.r.appspot.com/graphql"; /// How many times a report should attempt to send. @@ -40,16 +39,16 @@ struct ReportResponse { /// of data around various threads. #[derive(Clone, Debug)] pub struct GameReporterQueue { - pub http_client: ureq::Agent, + pub api_client: APIClient, pub iso_hash: Arc>, inner: Arc>>, } impl GameReporterQueue { /// Initializes and returns a new game reporter. - pub(crate) fn new(http_client: Agent) -> Self { + pub(crate) fn new(api_client: APIClient) -> Self { Self { - http_client, + api_client, iso_hash: Arc::new(Mutex::new(String::new())), inner: Arc::new(Mutex::new(VecDeque::new())), } @@ -90,7 +89,7 @@ impl GameReporterQueue { } })); - let res = execute_graphql_query(&self.http_client, mutation, variables, Some("abandonOnlineGame")); + let res = execute_graphql_query(&self.api_client, mutation, variables, Some("abandonOnlineGame")); match res { Ok(value) if value == "true" => { @@ -102,7 +101,7 @@ impl GameReporterQueue { } } -pub(crate) fn run_completion(http_client: ureq::Agent, receiver: Receiver) { +pub(crate) fn run_completion(api_client: APIClient, receiver: Receiver) { loop { // Watch for notification to do work match receiver.recv() { @@ -112,7 +111,7 @@ pub(crate) fn run_completion(http_client: ureq::Agent, receiver: Receiver { - report_completion(&http_client, uid, match_id, play_key, end_mode); + report_completion(&api_client, uid, match_id, play_key, end_mode); }, Ok(CompletionEvent::Shutdown) => { @@ -140,7 +139,7 @@ pub(crate) fn run_completion(http_client: ureq::Agent, receiver: Receiver { @@ -218,7 +217,7 @@ fn process_reports(queue: &GameReporterQueue, event: ProcessingEvent) { // (e.g, max attempts). We pass the locked queue over to work with the borrow checker // here, since otherwise we can't pop without some ugly block work to coerce letting // a mutable borrow drop. - match try_send_next_report(&mut *report_queue, event, &queue.http_client, &iso_hash) { + match try_send_next_report(&mut *report_queue, event, &queue.api_client, &iso_hash) { Ok(upload_url) => { // Pop the front of the queue. If we have a URL, chuck it all over // to the replay uploader. @@ -227,7 +226,7 @@ fn process_reports(queue: &GameReporterQueue, event: ProcessingEvent) { tracing::info!(target: Log::SlippiOnline, "Successfully sent report, popping from queue"); if let (Some(report), Some(upload_url)) = (report, upload_url) { - try_upload_replay_data(report.replay_data, upload_url, &queue.http_client); + try_upload_replay_data(report.replay_data, upload_url, &queue.api_client); } thread::sleep(Duration::ZERO) @@ -266,7 +265,7 @@ fn process_reports(queue: &GameReporterQueue, event: ProcessingEvent) { /// The true inner error, minus any metadata. #[derive(Debug)] enum ReportSendErrorKind { - Net(ureq::Error), + Net(slippi_gg_api::Error), JSON(serde_json::Error), GraphQL(String), NotSuccessful(String), @@ -287,7 +286,7 @@ struct ReportSendError { fn try_send_next_report( queue: &mut VecDeque, event: ProcessingEvent, - http_client: &ureq::Agent, + api_client: &APIClient, iso_hash: &str, ) -> Result, ReportSendError> { let report = (*queue).front_mut().expect("Reporter queue is empty yet it shouldn't be"); @@ -324,7 +323,7 @@ fn try_send_next_report( // Call execute_graphql_query and get the response body as a String. let response_body = - execute_graphql_query(http_client, mutation, variables, Some("reportOnlineGame")).map_err(|e| ReportSendError { + execute_graphql_query(api_client, mutation, variables, Some("reportOnlineGame")).map_err(|e| ReportSendError { is_last_attempt, sleep_ms: error_sleep_ms, kind: e, @@ -350,7 +349,7 @@ fn try_send_next_report( /// Prepares and executes a GraphQL query. fn execute_graphql_query( - http_client: &ureq::Agent, + api_client: &APIClient, query: &str, variables: Option, field: Option<&str>, @@ -367,7 +366,7 @@ fn execute_graphql_query( }; // Make the GraphQL request - let response = http_client + let response = api_client .post(GRAPHQL_URL) .send_json(&request_body) .map_err(ReportSendErrorKind::Net)?; @@ -427,7 +426,7 @@ fn add_slp_header_and_footer(data: Arc>>) -> Vec { } /// Attempts to compress and upload replay data to the url at `upload_url`. -fn try_upload_replay_data(data: Arc>>, upload_url: String, http_client: &ureq::Agent) { +fn try_upload_replay_data(data: Arc>>, upload_url: String, api_client: &APIClient) { let contents = add_slp_header_and_footer(data); let mut gzipped_data = vec![0u8; contents.len()]; // Resize to some initial size @@ -443,7 +442,7 @@ fn try_upload_replay_data(data: Arc>>, upload_url: String, http_cl gzipped_data.resize(res_size, 0); - let response = http_client + let response = api_client .put(upload_url.as_str()) .set("Content-Type", "application/octet-stream") .set("Content-Encoding", "gzip") diff --git a/slippi-gg-api/Cargo.toml b/slippi-gg-api/Cargo.toml new file mode 100644 index 0000000..f54bb4a --- /dev/null +++ b/slippi-gg-api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "slippi-gg-api" +description = "slippi.gg API client for internal calls." +version = "0.1.0" +authors = [ + "Slippi Team", + "Ryan McGrath " +] +repository = "" +edition = "2021" +publish = false + +[dependencies] +ureq = { workspace = true } +tracing = { workspace = true } + +[features] +default = ["ishiiruka"] +ishiiruka = [] +mainline = [] +playback = [] diff --git a/slippi-gg-api/src/lib.rs b/slippi-gg-api/src/lib.rs new file mode 100644 index 0000000..f13f407 --- /dev/null +++ b/slippi-gg-api/src/lib.rs @@ -0,0 +1,84 @@ +use std::io; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::ops::{Deref, DerefMut}; +use std::time::Duration; + +use ureq::{Agent, AgentBuilder, Resolver}; + +/// Re-export `ureq::Error` for simplicity. +pub type Error = ureq::Error; + +/// A DNS resolver that only accepts IPV4 connections. +struct Ipv4Resolver; + +impl Resolver for Ipv4Resolver { + /// Forces IPV4 addresses only. + fn resolve(&self, netloc: &str) -> io::Result> { + ToSocketAddrs::to_socket_addrs(netloc).map(|iter| { + let vec = iter.filter(|s| s.is_ipv4()).collect::>(); + + if vec.is_empty() { + tracing::warn!("Failed to get any IPV4 addresses. Does the DNS server support it?"); + } + + vec + }) + } +} + +/// A wrapper type that simply dereferences to a `ureq::Agent`. +/// +/// It's extracted purely for ease of debugging, and for segmenting +/// some initial setup code that would just be cumbersome to do in the +/// core EXI device initialization block. +/// +/// Anything that can be called on a `ureq::Agent` can be called on +/// this type. You can also clone this with little cost, and pass it freely +/// to other threads, as it manages itself under the hood with `Arc`. +#[derive(Clone, Debug)] +pub struct APIClient(Agent); + +impl APIClient { + /// Creates and initializes a new APIClient. + /// + /// The returned client will only resolve to IPV4 addresses at the moment + /// due to upstream issues with GCP flex instances and IPV6. + pub fn new(slippi_semver: &str) -> Self { + let _build = ""; + + #[cfg(feature = "mainline")] + let _build = "mainline"; + + #[cfg(feature = "ishiiruka")] + let _build = "ishiiruka"; + + #[cfg(feature = "playback")] + let _build = "playback"; + + // We set `max_idle_connections` to `5` to mimic how CURL was configured in + // the old C++ logic. This gets cloned and passed down into modules so that + // the underlying connection pool is shared. + let http_client = AgentBuilder::new() + .resolver(Ipv4Resolver) + .max_idle_connections(5) + .timeout(Duration::from_millis(5000)) + .user_agent(&format!("SlippiDolphin/{} ({}) (Rust)", _build, slippi_semver)) + .build(); + + Self(http_client) + } +} + +impl Deref for APIClient { + type Target = Agent; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for APIClient { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/user/Cargo.toml b/user/Cargo.toml index f75f0e0..531243e 100644 --- a/user/Cargo.toml +++ b/user/Cargo.toml @@ -20,5 +20,5 @@ dolphin-integrations = { path = "../dolphin" } open = "5" serde = { workspace = true } serde_json = { workspace = true } +slippi-gg-api = { path = "../slippi-gg-api" } tracing = { workspace = true } -ureq = { workspace = true } diff --git a/user/src/lib.rs b/user/src/lib.rs index dc37b04..1dbf268 100644 --- a/user/src/lib.rs +++ b/user/src/lib.rs @@ -4,9 +4,8 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; -use ureq::Agent; - // use dolphin_integrations::Log; +use slippi_gg_api::APIClient; mod chat; pub use chat::DEFAULT_CHAT_MESSAGES; @@ -44,7 +43,6 @@ impl UserInfo { /// Mostly checks to make sure we're not loading or receiving anything undesired. pub fn sanitize(&mut self) { if self.chat_messages.is_none() || self.chat_messages.as_ref().unwrap().len() != 16 { - // if self.chat_messages.len() != 16 { self.chat_messages = Some(chat::default()); } } @@ -58,7 +56,7 @@ impl UserInfo { /// where this stuff is called into from the C++ side. #[derive(Clone, Debug)] pub struct UserManager { - http_client: Agent, + api_client: APIClient, user: Arc>, user_json_path: Arc, slippi_semver: String, @@ -74,13 +72,13 @@ impl UserManager { /// this restriction via some assumptions. // @TODO: The semver param here should get refactored away in time once we've ironed out // how some things get persisted from the Dolphin side. Not a big deal to thread it for now. - pub fn new(http_client: Agent, user_json_path: PathBuf, slippi_semver: String) -> Self { + pub fn new(api_client: APIClient, user_json_path: PathBuf, slippi_semver: String) -> Self { let user = Arc::new(Mutex::new(UserInfo::default())); let user_json_path = Arc::new(user_json_path); let watcher = Arc::new(Mutex::new(UserInfoWatcher::new())); Self { - http_client, + api_client, user, user_json_path, slippi_semver, @@ -141,7 +139,7 @@ impl UserManager { /// Runs the `attempt_login` function on the calling thread. If you need this to run in the /// background, you want `watch_for_login` instead. pub fn attempt_login(&self) -> bool { - attempt_login(&self.http_client, &self.user, &self.user_json_path, &self.slippi_semver) + attempt_login(&self.api_client, &self.user, &self.user_json_path, &self.slippi_semver) } /// Kicks off a background handler for processing user authentication. @@ -149,7 +147,7 @@ impl UserManager { let mut watcher = self.watcher.lock().expect("Unable to acquire user watcher lock"); watcher.watch_for_login( - self.http_client.clone(), + self.api_client.clone(), self.user_json_path.clone(), self.user.clone(), &self.slippi_semver, @@ -218,7 +216,7 @@ impl UserManager { /// Checks for the existence of a `user.json` file and, if found, attempts to load and parse it. /// /// This returns a `bool` value so that the background thread can know whether to stop checking. -fn attempt_login(http_client: &Agent, user: &Arc>, user_json_path: &PathBuf, slippi_semver: &str) -> bool { +fn attempt_login(api_client: &APIClient, user: &Arc>, user_json_path: &PathBuf, slippi_semver: &str) -> bool { match std::fs::read_to_string(user_json_path) { Ok(contents) => match serde_json::from_str::(&contents) { Ok(mut info) => { @@ -231,7 +229,7 @@ fn attempt_login(http_client: &Agent, user: &Arc>, user_json_pat *lock = info; } - overwrite_from_server(http_client, user, uid, slippi_semver); + overwrite_from_server(api_client, user, uid, slippi_semver); return true; }, @@ -275,7 +273,7 @@ struct UserInfoAPIResponse { /// Calls out to the Slippi server and fetches the user info, patching up the user info object /// with any returned information. -fn overwrite_from_server(http_client: &Agent, user: &Arc>, uid: String, slippi_semver: &str) { +fn overwrite_from_server(api_client: &APIClient, user: &Arc>, uid: String, slippi_semver: &str) { let is_beta = match slippi_semver.contains("beta") { true => "-beta", false => "", @@ -286,7 +284,7 @@ fn overwrite_from_server(http_client: &Agent, user: &Arc>, uid: tracing::warn!(?url, "Fetching user info"); - match http_client.get(&url).call() { + match api_client.get(&url).call() { Ok(response) => match response.into_string() { Ok(body) => match serde_json::from_str::(&body) { Ok(info) => { diff --git a/user/src/watcher.rs b/user/src/watcher.rs index 11e2d9f..4077609 100644 --- a/user/src/watcher.rs +++ b/user/src/watcher.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use ureq::Agent; +use slippi_gg_api::APIClient; use super::{attempt_login, UserInfo}; @@ -28,7 +28,7 @@ impl UserInfoWatcher { /// Spins up (or re-spins-up) the background watcher thread for the `user.json` file. pub fn watch_for_login( &mut self, - http_client: Agent, + api_client: APIClient, user_json_path: Arc, user: Arc>, slippi_semver: &str, @@ -55,7 +55,7 @@ impl UserInfoWatcher { return; } - if attempt_login(&http_client, &user, &user_json_path, &slippi_semver) { + if attempt_login(&api_client, &user, &user_json_path, &slippi_semver) { return; }