diff --git a/Cargo.lock b/Cargo.lock index d67943c..862fba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,6 +718,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + [[package]] name = "memchr" version = "2.7.5" @@ -1098,10 +1104,12 @@ dependencies = [ "digest", "flate2", "hmac-sha256", + "md5", "nyquest", "serde", "serde_json", "snafu", + "strum 0.27.2", ] [[package]] @@ -1205,8 +1213,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf745ed831fe29ec1ff6cc8dfb443721c08e895c9a08fcaa1e2c6f09ec020d" dependencies = [ "nom", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror 1.0.69", ] @@ -1260,7 +1268,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -1276,6 +1293,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/sdgb-api/Cargo.toml b/sdgb-api/Cargo.toml index 2fbac9a..2c0b7be 100644 --- a/sdgb-api/Cargo.toml +++ b/sdgb-api/Cargo.toml @@ -11,12 +11,18 @@ snafu = { workspace = true } digest = "0.10.7" hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] } +# other utils chrono = "0.4.41" +strum = { version = "0.27.2", features = ["derive"] } +# network request nyquest = { version = "0.2.0", features = ["async", "json"] } +# (de)serialization serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.141" + flate2 = "1.1.2" cbc = "0.1.2" aes = "0.8.4" +md5 = "0.8.0" diff --git a/sdgb-api/src/title/error/mod.rs b/sdgb-api/src/title/error/mod.rs index edc6120..991bb95 100644 --- a/sdgb-api/src/title/error/mod.rs +++ b/sdgb-api/src/title/error/mod.rs @@ -7,9 +7,14 @@ pub enum ApiError { PadError { error: PadError }, #[snafu(display("unpad data: {error}"))] UnpadError { error: UnpadError }, + #[snafu(display("io error: {source}"))] #[snafu(context(false))] IOError { source: std::io::Error }, + + #[snafu(display("json error: {source}"))] + #[snafu(context(false))] + JSONError { source: serde_json::Error }, } impl From for ApiError { diff --git a/sdgb-api/src/title/methods/mod.rs b/sdgb-api/src/title/methods/mod.rs new file mode 100644 index 0000000..129e141 --- /dev/null +++ b/sdgb-api/src/title/methods/mod.rs @@ -0,0 +1,63 @@ +#[derive(strum::IntoStaticStr)] +pub enum APIMethod { + GetGameChargeApi, + GetGameEventApi, + GetGameNgMusicIdApi, + GetGameNgWordListApi, + GetGameRankingApi, + GetGameSettingApi, + GetGameTournamentInfoApi, + GetTransferFriendApi, + GetUserActivityApi, + GetUserCardApi, + GetUserCharacterApi, + GetUserChargeApi, + GetUserCourseApi, + GetUserDataApi, + GetUserExtendApi, + GetUserFavoriteApi, + GetUserFavoriteItemApi, + GetUserFriendSeasonRankingApi, + GetUserGhostApi, + GetUserItemApi, + GetUserLoginBonusApi, + GetUserMapApi, + GetUserMusicApi, + GetUserOptionApi, + GetUserPortraitApi, + GetUserPreviewApi, + GetUserRatingApi, + GetUserRecommendRateMusicApi, + GetUserRecommendSelectMusicApi, + GetUserRegionApi, + GetUserScoreRankingApi, + Ping, + UploadUserPhotoApi, + UploadUserPlaylogApi, + UploadUserPortraitApi, + UpsertClientBookkeepingApi, + UpsertClientSettingApi, + UpsertClientTestmodeApi, + UpsertClientUploadApi, + UpsertUserAllApi, + UpsertUserChargelogApi, + UserLoginApi, + UserLogoutApi, +} + +#[cfg(test)] +mod _test { + use crate::title::{MaiVersionExt, Sdgb1_50, methods::APIMethod}; + + #[test] + fn test_obfuscate_1_50() { + assert_eq!( + Sdgb1_50::api_hash(APIMethod::Ping), + "250b3482854e7697de7d8eb6ea1fabb1" + ); + assert_eq!( + Sdgb1_50::api_hash(APIMethod::GetUserPreviewApi), + "004cf848f96d393a5f2720101e30b93d" + ); + } +} diff --git a/sdgb-api/src/title/mod.rs b/sdgb-api/src/title/mod.rs index 267229a..2b0f58b 100644 --- a/sdgb-api/src/title/mod.rs +++ b/sdgb-api/src/title/mod.rs @@ -1,17 +1,67 @@ -use crate::title::error::ApiError; +use std::fmt::Display; + +use crate::title::methods::APIMethod; pub mod encryption; +pub mod methods; +pub mod model; + mod error; +pub use error::ApiError; +use nyquest::{ + Body, + r#async::Request, + header::{ACCEPT_ENCODING, CONTENT_ENCODING, EXPECT, USER_AGENT}, +}; +use serde::Serialize; pub trait MaiVersion { const AES_KEY: &[u8; 32]; const AES_IV: &[u8; 16]; - const OBFUSECATE_PARAM: &str; + const OBFUSECATE_SUFFIX: &str; + const VERSION: &str; } pub trait MaiVersionExt: MaiVersion { fn encode(data: impl AsRef<[u8]>) -> Result, ApiError>; fn decode(data: impl AsMut<[u8]>) -> Result, ApiError>; + + fn api_hash(api: APIMethod) -> String { + let api_name: &str = api.into(); + + let mut md5 = md5::Context::new(); + md5.consume(api_name); + md5.consume(Self::OBFUSECATE_SUFFIX); + let digest = md5.finalize(); + + format!("{digest:x}") + } + + fn api_request( + api: APIMethod, + agent_extra: impl Display, + data: D, + ) -> Result + where + D: Serialize, + { + let json = serde_json::to_vec(&data)?; + let payload = Self::encode(json)?; + + let api_hash = Self::api_hash(api); + let req = Request::post(format!( + "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/{api_hash}" + )) + .with_body(Body::json_bytes(payload)) + .with_header(USER_AGENT, format!("{api_hash}#{agent_extra}")) + .with_header("Mai-Encoding", Self::VERSION) + .with_header(ACCEPT_ENCODING, "") + .with_header("Charset", "UTF-8") + .with_header(CONTENT_ENCODING, "deflate") + .with_header(EXPECT, "100-continue"); + + Ok(req) + } } pub struct Sdgb1_40; @@ -20,10 +70,14 @@ pub struct Sdgb1_50; impl MaiVersion for Sdgb1_40 { const AES_KEY: &[u8; 32] = b"n7bx6:@Fg_:2;5E89Phy7AyIcpxEQ:R@"; const AES_IV: &[u8; 16] = b";;KjR1C3hgB1ovXa"; - const OBFUSECATE_PARAM: &str = "BEs2D5vW"; + const OBFUSECATE_SUFFIX: &str = "MaimaiChnBEs2D5vW"; + + const VERSION: &str = "1.40"; } impl MaiVersion for Sdgb1_50 { const AES_KEY: &[u8; 32] = b"a>32bVP7v<63BVLkY[xM>daZ1s9MBP Result<(), Box> { let client = ClientBuilder::default().build_async().await?; match cmd.command { + commands::Commands::Ping => { + let req = Sdgb1_50::api_request(APIMethod::Ping, "", Ping)?; + let data = client.request(req).await?.bytes().await?; + let decoded = Sdgb1_50::decode(data)?; + info!("resp: {:?}", String::from_utf8_lossy(&decoded)); + } commands::Commands::QRLogin { ref qrcode_content } => { let qrcode = QRCode { qrcode_content }; match qrcode.login(&client).await {