From 6c3fbe33782def0050e88eb725564ed07058b981 Mon Sep 17 00:00:00 2001 From: beerpiss Date: Wed, 15 Nov 2023 18:39:13 +0700 Subject: [PATCH 1/4] feat: add feature to allow FAILED to take precedence over lamps I don't know why you would want this, but an ALL JUSTICE showing as FAILED is funny. --- res/saekawa.toml | 2 ++ src/configuration.rs | 11 +++++++++- src/saekawa.rs | 48 ++++++++++++++++++++++++++------------------ src/types/tachi.rs | 12 +++++------ 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/res/saekawa.toml b/res/saekawa.toml index 300f32b..a294653 100644 --- a/res/saekawa.toml +++ b/res/saekawa.toml @@ -3,6 +3,8 @@ enable = true # Whether the hook should export your class medals and emblems or not. export_class = true +# Whether FAILED should override FULL COMBO and ALL JUSTICE. +fail_over_lamp = false # Timeout for web requests, in milliseconds timeout = 3000 diff --git a/src/configuration.rs b/src/configuration.rs index 702e7f9..e2eec4d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -28,8 +28,13 @@ impl Configuration { pub struct GeneralConfiguration { #[serde(default = "default_true")] pub enable: bool, - #[serde(default)] + + #[serde(default = "default_true")] pub export_class: bool, + + #[serde(default = "default_false")] + pub fail_over_lamp: bool, + #[serde(default = "default_timeout")] pub timeout: u64, } @@ -38,6 +43,10 @@ fn default_true() -> bool { true } +fn default_false() -> bool { + false +} + fn default_timeout() -> u64 { 3000 } diff --git a/src/saekawa.rs b/src/saekawa.rs index 9c6752c..68bd177 100644 --- a/src/saekawa.rs +++ b/src/saekawa.rs @@ -14,7 +14,7 @@ use winapi::{ }; use crate::{ - helpers::{call_tachi, read_hinternet_url, read_potentially_deflated_buffer, self}, + helpers::{call_tachi, read_hinternet_url, read_potentially_deflated_buffer, request_tachi}, types::{ game::UpsertUserAllRequest, tachi::{ClassEmblem, Import, ImportClasses, ImportScore}, @@ -33,7 +33,7 @@ pub fn hook_init() -> Result<()> { return Ok(()); } - let resp: serde_json::Value = helpers::request_tachi("GET", TACHI_STATUS_URL.as_str(), None::<()>)?; + let resp: serde_json::Value = request_tachi("GET", TACHI_STATUS_URL.as_str(), None::<()>)?; let user_id = resp["body"]["whoami"] .as_u64() .ok_or(anyhow::anyhow!("Couldn't parse user from Tachi response"))?; @@ -47,14 +47,13 @@ pub fn hook_init() -> Result<()> { }; unsafe { - DetourWriteData.initialize(winhttpwritedata, move |a, b, c, d| { - winhttpwritedata_hook(a, b, c, d) - })?; - - DetourWriteData.enable()?; + DetourWriteData + .initialize(winhttpwritedata, winhttpwritedata_hook)? + .enable()?; }; info!("Hook successfully initialized"); + Ok(()) } @@ -68,7 +67,7 @@ pub fn hook_release() -> Result<()> { Ok(()) } -unsafe fn winhttpwritedata_hook( +fn winhttpwritedata_hook( h_request: HINTERNET, lp_buffer: LPCVOID, dw_number_of_bytes_to_write: DWORD, @@ -76,12 +75,14 @@ unsafe fn winhttpwritedata_hook( ) -> BOOL { debug!("hit winhttpwritedata"); - let orig = || DetourWriteData.call( - h_request, - lp_buffer, - dw_number_of_bytes_to_write, - lpdw_number_of_bytes_written, - ); + let orig = || unsafe { + DetourWriteData.call( + h_request, + lp_buffer, + dw_number_of_bytes_to_write, + lpdw_number_of_bytes_written, + ) + }; let url = match read_hinternet_url(h_request) { Ok(url) => url, @@ -92,10 +93,12 @@ unsafe fn winhttpwritedata_hook( }; debug!("winhttpwritedata URL: {url}"); - let request_body = match read_potentially_deflated_buffer( - lp_buffer as *const u8, - dw_number_of_bytes_to_write as usize, - ) { + let request_body = match unsafe { + read_potentially_deflated_buffer( + lp_buffer as *const u8, + dw_number_of_bytes_to_write as usize, + ) + } { Ok(data) => data, Err(err) => { error!("There was an error reading the request body: {:#}", err); @@ -141,7 +144,9 @@ unsafe fn winhttpwritedata_hook( .user_playlog_list .into_iter() .filter_map(|playlog| { - if let Ok(score) = ImportScore::try_from(playlog) { + if let Ok(score) = + ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp) + { if score.difficulty.as_str() == "WORLD'S END" { return None; } @@ -157,7 +162,10 @@ unsafe fn winhttpwritedata_hook( return orig(); } - if classes.clone().is_some_and(|v| v.dan.is_none() && v.emblem.is_none()) { + if classes + .clone() + .is_some_and(|v| v.dan.is_none() && v.emblem.is_none()) + { return orig(); } } diff --git a/src/types/tachi.rs b/src/types/tachi.rs index aa55c71..d05792c 100644 --- a/src/types/tachi.rs +++ b/src/types/tachi.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Result}; use chrono::{FixedOffset, NaiveDateTime, TimeZone}; use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize}; @@ -107,11 +107,11 @@ pub struct OptionalMetrics { pub max_combo: u32, } -impl TryFrom for ImportScore { - type Error = anyhow::Error; - - fn try_from(p: UserPlaylog) -> Result { - let lamp = if p.is_all_justice { +impl ImportScore { + pub fn try_from_playlog(p: UserPlaylog, fail_over_lamp: bool) -> Result { + let lamp = if !p.is_clear && fail_over_lamp { + TachiLamp::Failed + } else if p.is_all_justice { if p.judge_justice + p.judge_attack + p.judge_guilty == 0 { TachiLamp::AllJusticeCritical } else { From a445d87c6e8a45c4953cf0577dc752c1a4c8c1bc Mon Sep 17 00:00:00 2001 From: beerpiss Date: Wed, 15 Nov 2023 18:51:58 +0700 Subject: [PATCH 2/4] feat: no more stringly typed difficulties --- src/saekawa.rs | 12 ++++-------- src/types/tachi.rs | 48 ++++++++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/saekawa.rs b/src/saekawa.rs index 68bd177..bde1853 100644 --- a/src/saekawa.rs +++ b/src/saekawa.rs @@ -17,7 +17,7 @@ use crate::{ helpers::{call_tachi, read_hinternet_url, read_potentially_deflated_buffer, request_tachi}, types::{ game::UpsertUserAllRequest, - tachi::{ClassEmblem, Import, ImportClasses, ImportScore}, + tachi::{ClassEmblem, Import, ImportClasses, ImportScore, Difficulty}, }, CONFIGURATION, TACHI_IMPORT_URL, TACHI_STATUS_URL, }; @@ -144,13 +144,9 @@ fn winhttpwritedata_hook( .user_playlog_list .into_iter() .filter_map(|playlog| { - if let Ok(score) = - ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp) - { - if score.difficulty.as_str() == "WORLD'S END" { - return None; - } - Some(score) + let result = ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp); + if result.as_ref().is_ok_and(|v| v.difficulty != Difficulty::WorldsEnd) { + result.ok() } else { None } diff --git a/src/types/tachi.rs b/src/types/tachi.rs index d05792c..d40d5e7 100644 --- a/src/types/tachi.rs +++ b/src/types/tachi.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use chrono::{FixedOffset, NaiveDateTime, TimeZone}; use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize}; @@ -68,7 +68,7 @@ pub struct ImportScore { pub lamp: TachiLamp, pub match_type: String, pub identifier: String, - pub difficulty: String, + pub difficulty: Difficulty, pub time_achieved: u128, pub judgements: Judgements, pub optional: OptionalMetrics, @@ -93,6 +93,28 @@ pub enum TachiLamp { AllJusticeCritical = 4, } +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, TryFromPrimitive)] +#[repr(u32)] +pub enum Difficulty { + #[serde(rename = "BASIC")] + Basic = 0, + + #[serde(rename = "ADVANCED")] + Advanced = 1, + + #[serde(rename = "EXPERT")] + Expert = 2, + + #[serde(rename = "MASTER")] + Master = 3, + + #[serde(rename = "ULTIMA")] + Ultima = 4, + + #[serde(rename = "WORLD'S END")] + WorldsEnd = 5, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Judgements { pub jcrit: u32, @@ -133,23 +155,11 @@ impl ImportScore { }; let rom_major_version = p.rom_version.split('.').next().unwrap_or("2"); - let difficulty = match p.level { - 0 => "BASIC", - 1 => "ADVANCED", - 2 => "EXPERT", - 3 => "MASTER", - 4 => if rom_major_version == "2" { - "ULTIMA" - } else { - "WORLD'S END" - }, - 5 => if rom_major_version == "2" { - "WORLD'S END" - } else { - return Err(anyhow!("difficulty index '5' should not be possible on rom_version {rom_major_version}.")); - }, - _ => return Err(anyhow!("unknown difficulty index {level} on major version {rom_major_version}", level=p.level)), - }.to_string(); + let difficulty = if rom_major_version == "1" && p.level == 4 { + Difficulty::WorldsEnd + } else { + Difficulty::try_from(p.level)? + }; let datetime = NaiveDateTime::parse_from_str(&p.user_play_date, "%Y-%m-%d %H:%M:%S")?; let jst_offset = From 07028d9dc74500bc52d2dd4921957140c47e069b Mon Sep 17 00:00:00 2001 From: beerpiss Date: Wed, 15 Nov 2023 19:02:34 +0700 Subject: [PATCH 3/4] fix: less dumb zlib decoder there are a bunch of zlib headers, so letting the zlib library deal with it is probably safer --- src/helpers.rs | 29 +++++++++++++++++++---------- src/saekawa.rs | 20 +++++++++++--------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 87ac19c..92e1e94 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -125,17 +125,26 @@ pub fn read_hinternet_url(handle: HINTERNET) -> Result { return Err(anyhow!("Could not get URL from HINTERNET handle: {ec}")); } -pub unsafe fn read_potentially_deflated_buffer(buf: *const u8, len: usize) -> Result { - let mut slice = std::slice::from_raw_parts(buf, len); +pub fn read_potentially_deflated_buffer(buf: *const u8, len: usize) -> Result { + let mut slice = unsafe { std::slice::from_raw_parts(buf, len) }; let mut ret = String::new(); - let _ = if slice[0] == 120 && slice[1] == 156 { - // Just a really dumb check if the request is sent over zlib or not - let mut decoder = ZlibDecoder::new(slice); - decoder.read_to_string(&mut ret) - } else { - slice.read_to_string(&mut ret) - }?; + let mut decoder = ZlibDecoder::new(slice); + let zlib_result = decoder.read_to_string(&mut ret); + if zlib_result.is_ok() { + return Ok(ret); + } - Ok(ret) + ret.clear(); + let result = slice.read_to_string(&mut ret); + if result.is_ok() { + return Ok(ret); + } + + // Unwrapping here is fine, if result was Ok we wouldn't reach this place. + Err(anyhow!( + "Could not decode contents of buffer as both DEFLATE-compressed ({:#}) and plaintext ({:#}) UTF-8 string.", + zlib_result.err().expect("This shouldn't happen, if Result was Ok the string should have been returned earlier."), + result.err().expect("This shouldn't happen, if Result was Ok the string should have been returned earlier."), + )) } diff --git a/src/saekawa.rs b/src/saekawa.rs index bde1853..bd6a75c 100644 --- a/src/saekawa.rs +++ b/src/saekawa.rs @@ -17,7 +17,7 @@ use crate::{ helpers::{call_tachi, read_hinternet_url, read_potentially_deflated_buffer, request_tachi}, types::{ game::UpsertUserAllRequest, - tachi::{ClassEmblem, Import, ImportClasses, ImportScore, Difficulty}, + tachi::{ClassEmblem, Difficulty, Import, ImportClasses, ImportScore}, }, CONFIGURATION, TACHI_IMPORT_URL, TACHI_STATUS_URL, }; @@ -93,12 +93,10 @@ fn winhttpwritedata_hook( }; debug!("winhttpwritedata URL: {url}"); - let request_body = match unsafe { - read_potentially_deflated_buffer( - lp_buffer as *const u8, - dw_number_of_bytes_to_write as usize, - ) - } { + let request_body = match read_potentially_deflated_buffer( + lp_buffer as *const u8, + dw_number_of_bytes_to_write as usize, + ) { Ok(data) => data, Err(err) => { error!("There was an error reading the request body: {:#}", err); @@ -144,8 +142,12 @@ fn winhttpwritedata_hook( .user_playlog_list .into_iter() .filter_map(|playlog| { - let result = ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp); - if result.as_ref().is_ok_and(|v| v.difficulty != Difficulty::WorldsEnd) { + let result = + ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp); + if result + .as_ref() + .is_ok_and(|v| v.difficulty != Difficulty::WorldsEnd) + { result.ok() } else { None From 183995a002bbcfffdfa998ae3a38def5eea4e39e Mon Sep 17 00:00:00 2001 From: beerpiss Date: Wed, 15 Nov 2023 21:54:37 +0700 Subject: [PATCH 4/4] feat: verify API key have proper permissions --- src/crypto.rs | 0 src/saekawa.rs | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 src/crypto.rs diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/saekawa.rs b/src/saekawa.rs index bd6a75c..94c24f7 100644 --- a/src/saekawa.rs +++ b/src/saekawa.rs @@ -38,6 +38,16 @@ pub fn hook_init() -> Result<()> { .as_u64() .ok_or(anyhow::anyhow!("Couldn't parse user from Tachi response"))?; + let mut permissions = resp["body"]["permissions"] + .as_array() + .ok_or(anyhow!("Couldn't parse permissions from Tachi response"))? + .into_iter() + .filter_map(|v| v.as_str()); + + if permissions.all(|v| v != "submit_score") { + return Err(anyhow!("API key has insufficient permission. The permission submit_score must be set.")); + } + info!("Logged in to Tachi with userID {user_id}"); let winhttpwritedata = unsafe {