From 07489518dddbfe1ec6cf98a7d03c52833fa8b2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Sat, 13 Jun 2026 10:48:34 +0200 Subject: [PATCH 1/6] feat: add title mgt and protobuf for ipc --- Cargo.lock | 119 +++++++++++++++++++++++++++ Cargo.toml | 2 +- proto/xodus.gamingservices.proto | 20 +++++ xodus-cli/src/main.rs | 3 +- xodus-service/Cargo.toml | 6 ++ xodus-service/src/main.rs | 3 + xodus/Cargo.toml | 5 ++ xodus/build.rs | 5 ++ xodus/src/api/xbox/auth.rs | 64 +++++++++++++++ xodus/src/api/xbox/mod.rs | 136 +------------------------------ xodus/src/api/xbox/title.rs | 59 ++++++++++++++ xodus/src/lib.rs | 8 ++ xodus/src/models.rs | 1 + xodus/src/models/xbox.rs | 99 ++++++++++++++++++++++ 14 files changed, 395 insertions(+), 135 deletions(-) create mode 100644 proto/xodus.gamingservices.proto create mode 100644 xodus-service/Cargo.toml create mode 100644 xodus-service/src/main.rs create mode 100644 xodus/build.rs create mode 100644 xodus/src/api/xbox/auth.rs create mode 100644 xodus/src/api/xbox/title.rs create mode 100644 xodus/src/models/xbox.rs diff --git a/Cargo.lock b/Cargo.lock index 7bab254..a373072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,6 +500,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -1320,6 +1330,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -1427,6 +1443,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flagset" version = "0.4.7" @@ -1808,6 +1830,19 @@ dependencies = [ "system-deps", ] +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -2267,6 +2302,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2613,6 +2657,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.18" @@ -3180,6 +3230,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "phf" version = "0.13.1" @@ -3433,6 +3494,57 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck 0.5.0", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + [[package]] name = "quick-xml" version = "0.40.1" @@ -5713,10 +5825,13 @@ dependencies = [ "chrono", "dbus-secret-service-keyring-store", "ed25519-dalek", + "globset", "hmac 0.12.1", "keyring-core", "log", "num_enum", + "prost", + "prost-build", "quick-xml", "rand 0.10.1", "reqwest", @@ -5750,6 +5865,10 @@ dependencies = [ "xodus", ] +[[package]] +name = "xodus-service" +version = "0.1.0" + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 005c640..45e08ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["xodus", "xodus-cli"] +members = ["xodus", "xodus-cli", "xodus-service"] [workspace.dependencies] chrono = "0.4.42" diff --git a/proto/xodus.gamingservices.proto b/proto/xodus.gamingservices.proto new file mode 100644 index 0000000..a96b098 --- /dev/null +++ b/proto/xodus.gamingservices.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package xodus.gamingservices; + +message XSTSTokenRequest { + string url = 1; +} + +message XSTSTokenResponse { + string token = 1; + uint64 expiry = 2; + string relying_party = 3; + optional TitleSignaturePolicy policy = 4; +} + +message TitleSignaturePolicy { + repeated string algorithms = 1; + repeated uint64 max_body_bytes = 2; + repeated string signature_types = 3; +} \ No newline at end of file diff --git a/xodus-cli/src/main.rs b/xodus-cli/src/main.rs index 9536b21..e75a150 100644 --- a/xodus-cli/src/main.rs +++ b/xodus-cli/src/main.rs @@ -3,7 +3,6 @@ mod commands; mod device; mod user; mod webview; -use xodus::xal::client_params::CLIENT_WINDOWS; #[derive(Subcommand)] enum SubCommand { @@ -39,7 +38,7 @@ struct CliArgs { async fn main() { env_logger::init_from_env("XODUS_LOG"); let client = reqwest::ClientBuilder::new() - .user_agent(CLIENT_WINDOWS().user_agent) + .user_agent(format!("xodus-cli/{}", env!("CARGO_PKG_VERSION"))) .connection_verbose(true) .build() .unwrap(); diff --git a/xodus-service/Cargo.toml b/xodus-service/Cargo.toml new file mode 100644 index 0000000..c091d6e --- /dev/null +++ b/xodus-service/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "xodus-service" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/xodus-service/src/main.rs b/xodus-service/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/xodus-service/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/xodus/Cargo.toml b/xodus/Cargo.toml index c4c753f..9436e09 100644 --- a/xodus/Cargo.toml +++ b/xodus/Cargo.toml @@ -28,8 +28,13 @@ cbc = "0.2.1" url = "2.5.8" aes-keywrap = "0.9.0" num_enum = "0.7.6" +prost = "0.14.4" +globset = "0.4.18" [dev-dependencies] rsa = "0.9.10" bergshamra = "0.5.1" ed25519-dalek = "2.2.0" + +[build-dependencies] +prost-build = "0.14.4" diff --git a/xodus/build.rs b/xodus/build.rs new file mode 100644 index 0000000..a4ecdb7 --- /dev/null +++ b/xodus/build.rs @@ -0,0 +1,5 @@ +use std::io::Result; +fn main() -> Result<()> { + prost_build::compile_protos(&["../proto/xodus.gamingservices.proto"], &["../proto"])?; + Ok(()) +} \ No newline at end of file diff --git a/xodus/src/api/xbox/auth.rs b/xodus/src/api/xbox/auth.rs new file mode 100644 index 0000000..483f123 --- /dev/null +++ b/xodus/src/api/xbox/auth.rs @@ -0,0 +1,64 @@ +use crate::models::xbox::{ + UserAuthProperties, UserAuthRequest, XstsPropertyBag, XstsRequest, XstsResponse, +}; + +pub async fn authenticate_xbox_user( + client: &reqwest::Client, + rps_ticket: String, +) -> reqwest::Result { + let body = UserAuthRequest { + relying_party: "http://auth.xboxlive.com".to_string(), + token_type: "JWT".to_string(), + properties: UserAuthProperties { + auth_method: "RPS".to_string(), + site_name: "user.auth.xboxlive.com".to_string(), + rps_ticket, + }, + }; + + let resp = client + .post("https://user.auth.xboxlive.com/user/authenticate") + .header("Content-Type", "application/json") + .header("x-xbl-contract-version", "1") + .json(&body) + .send() + .await? + .error_for_status()?; + + resp.json().await +} + +pub async fn request_xsts_token( + client: &reqwest::Client, + token: String, + relying_party: &str, +) -> reqwest::Result { + let body = XstsRequest { + relying_party: Some(relying_party.to_string()), + token_type: Some("JWT".to_string()), + properties: XstsPropertyBag { + user_tokens: Some(vec![token]), + sandbox_id: Some("RETAIL".to_string()), + delegation_token: None, + service_token: None, + }, + }; + + let resp = client + .post("https://xsts.auth.xboxlive.com/xsts/authorize") + .header("Content-Type", "application/json") + .header("x-xbl-contract-version", "1") + .json(&body) + .send() + .await? + .error_for_status()?; + + resp.json().await +} + +pub fn get_xsts_auth_header(xsts: XstsResponse) -> String { + let uhs = xsts + .user_hash() + .expect("XSTS response missing xui claim"); + format!("XBL3.0 x={uhs};{}", xsts.token) +} diff --git a/xodus/src/api/xbox/mod.rs b/xodus/src/api/xbox/mod.rs index f60b7d5..de93f29 100644 --- a/xodus/src/api/xbox/mod.rs +++ b/xodus/src/api/xbox/mod.rs @@ -2,140 +2,12 @@ use crate::models::{ live::ExchangeUserTokenOutcome, secrets::{LegacyToken, Token}, soap, + xbox::XstsResponse, }; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize)] -#[serde(rename_all = "PascalCase")] -struct UserAuthRequest { - relying_party: String, - token_type: String, - properties: UserAuthProperties, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "PascalCase")] -struct UserAuthProperties { - auth_method: String, - site_name: String, - rps_ticket: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct XstsResponse { - token: String, - display_claims: DisplayClaims, -} - -#[derive(Debug, Deserialize)] -struct DisplayClaims { - xui: Vec, -} - -#[derive(Debug, Deserialize)] -struct XuiClaim { - uhs: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "PascalCase")] -struct XstsPropertyBag { - #[serde(skip_serializing_if = "Option::is_none")] - service_token: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - user_tokens: Option>, - - #[serde(rename = "SandboxId", skip_serializing_if = "Option::is_none")] - sandbox_id: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - delegation_token: Option, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "PascalCase")] -struct XstsRequest { - #[serde(skip_serializing_if = "Option::is_none")] - relying_party: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - token_type: Option, - - properties: XstsPropertyBag, -} - -pub async fn authenticate_xbox_user( - client: &reqwest::Client, - rps_ticket: String, -) -> reqwest::Result { - let body = UserAuthRequest { - relying_party: "http://auth.xboxlive.com".to_string(), - token_type: "JWT".to_string(), - properties: UserAuthProperties { - auth_method: "RPS".to_string(), - site_name: "user.auth.xboxlive.com".to_string(), - rps_ticket, - }, - }; - - let resp = client - .post("https://user.auth.xboxlive.com/user/authenticate") - .header("User-Agent", "xal-go/0.0.0") - .header("Content-Type", "application/json") - .header("x-xbl-contract-version", "1") - .json(&body) - .send() - .await? - .error_for_status()?; - - resp.json().await -} - -pub async fn request_xsts_token( - client: &reqwest::Client, - token: String, - relying_party: &str, -) -> reqwest::Result { - let body = XstsRequest { - relying_party: Some(relying_party.to_string()), - token_type: Some("JWT".to_string()), - properties: XstsPropertyBag { - user_tokens: Some(vec![token]), - sandbox_id: Some("RETAIL".to_string()), - delegation_token: None, - service_token: None, - }, - }; - - let rbody = serde_json::to_vec(&body).unwrap(); - - let resp = client - .post("https://xsts.auth.xboxlive.com/xsts/authorize") - .header("User-Agent", "xal-go/0.0.0") - .header("Content-Type", "application/json") - .header("x-xbl-contract-version", "1") - .body(rbody) - .send() - .await? - .error_for_status()?; - - let text = resp.text().await?; - - let parsed = serde_json::from_str::(&text).unwrap(); - Ok(parsed) -} - -pub fn get_xsts_auth_header(xsts: XstsResponse) -> String { - let uhs = xsts - .display_claims - .xui - .first() - .map(|claim| claim.uhs.as_str()) - .expect("XSTS response missing xui claim"); - format!("XBL3.0 x={uhs};{}", xsts.token) -} +pub mod auth; +pub mod title; +pub use auth::{authenticate_xbox_user, get_xsts_auth_header, request_xsts_token}; pub async fn run( client: &reqwest::Client, diff --git a/xodus/src/api/xbox/title.rs b/xodus/src/api/xbox/title.rs new file mode 100644 index 0000000..6dd18d5 --- /dev/null +++ b/xodus/src/api/xbox/title.rs @@ -0,0 +1,59 @@ +use crate::models::xbox::{TitleMgtEndPoint, TitleMgtResponse}; + +pub async fn get_title_management(client: &reqwest::Client) -> reqwest::Result { + let response = client + .get("https://title.mgt.xboxlive.com/titles/default/endpoints?type=1") + .send() + .await?; + response.json().await +} + +pub fn get_endpoint<'a>(url: &str, response: &'a TitleMgtResponse) -> Option<&'a TitleMgtEndPoint> { + let parsed_url = reqwest::Url::parse(url).ok()?; + let filtered: Vec<&TitleMgtEndPoint> = response + .end_points + .iter() + .filter(|e| e.protocol == parsed_url.scheme()) + .collect(); + let hosts: Vec = filtered + .iter() + .map(|e| globset::Glob::new(&e.host).expect("Unsupported glob")) + .collect(); + let set = globset::GlobSet::new(hosts).ok()?; + let matched = set.matches(parsed_url.host_str().unwrap()); + let matched: Vec<&TitleMgtEndPoint> = matched.into_iter().map(|m| filtered[m]).collect(); + println!("Matched: {matched:?}"); + let index = matched + .iter() + .max_by_key(|&&pat| pat.host.chars().filter(|&c| c != '*').count()) + .copied(); + + index +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_endpoint_cases() { + let string = r##"{"EndPoints":[{"Protocol":"https","Host":"musicdelivery-ssl.xboxlive.com","HostType":"fqdn","RelyingParty":"http://music.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"cloudcollection-ssl.xboxlive.com","HostType":"fqdn","RelyingParty":"http://music.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"websockets.platform.bing.com","HostType":"fqdn","RelyingParty":"http://platform.bing.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"websockets.platform.bing-int.com","HostType":"fqdn","RelyingParty":"http://platform.bing.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"inventory.xboxlive.com","HostType":"fqdn","RelyingParty":"http://licensing.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"licensing.xboxlive.com","HostType":"fqdn","RelyingParty":"http://licensing.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"accountstroubleshooter.xboxlive.com","HostType":"fqdn","RelyingParty":"http://accounts.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"gamertag.xboxlive.com","HostType":"fqdn","RelyingParty":"http://accounts.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"help.ui.xboxlive.com","HostType":"fqdn","RelyingParty":"http://uxservices.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.ui.xboxlive.com","HostType":"wildcard"},{"Protocol":"https","Host":"data-vef.xboxlive.com","HostType":"fqdn","RelyingParty":"http://data-vef.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"update.xboxlive.com","HostType":"fqdn","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"updatepc.xboxlive.com","HostType":"fqdn","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"update-cdn.xboxlive.com","HostType":"fqdn","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"packages.xboxlive.com","HostType":"fqdn","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"packagespc.xboxlive.com","HostType":"fqdn","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"instance.mgt.xboxlive.com","HostType":"fqdn","RelyingParty":"http://instance.mgt.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":2},{"Protocol":"https","Host":"device.mgt.xboxlive.com","HostType":"fqdn","RelyingParty":"http://device.mgt.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"device.mgt.xboxlive.com","HostType":"fqdn","Path":"/registrations/bestv","RelyingParty":"http://bestv.device.mgt.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"device.mgt.xboxlive.com","HostType":"fqdn","Path":"/devices/current/unlock","RelyingParty":"http://unlock.device.mgt.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"xkms.xboxlive.com","HostType":"fqdn","RelyingParty":"http://xkms.xboxlive.com","TokenType":"JWT","MinTlsVersion":"1.2","SignaturePolicyIndex":0},{"Protocol":"https","Host":"privileges.xboxlive.com","HostType":"fqdn","RelyingParty":"http://banning.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"privileges.xboxlive.com","HostType":"fqdn","Path":"/upsell","RelyingParty":"http://xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"settings.xboxlive.com","HostType":"fqdn","RelyingParty":"http://xboxlive.com","TokenType":"JWT","ServerCertIndex":[0,1],"SignaturePolicyIndex":1},{"Protocol":"https","Host":"*.experimentation.xboxlive.com","HostType":"wildcard","RelyingParty":"http://experimentation.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":1},{"Protocol":"https","Host":"*.xboxlive.com","HostType":"wildcard","RelyingParty":"http://xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xboxlive.com","HostType":"fqdn","RelyingParty":"http://xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"http","Host":"*.xboxlive.com","HostType":"wildcard"},{"Protocol":"https","Host":"xaaa.bbtv.cn","HostType":"fqdn","Path":"/xboxsms/OOBEService/AuthorizationStatus","RelyingParty":"http://bestvrp.bestv.com/","SubRelyingParty":"http://www.bestv.com.cn/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.data.microsoft.com","HostType":"wildcard","RelyingParty":"http://vortex.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"*.vortex-win-sandbox.data.microsoft.com","HostType":"wildcard","RelyingParty":"http://vortex-sbx.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"*.vortex-sandbox.data.microsoft.com","HostType":"wildcard","RelyingParty":"http://vortex-sbx.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"vortex-events.xboxlive.com","HostType":"fqdn","RelyingParty":"http://events.xboxlive.com","TokenType":"JWT"},{"Protocol":"https","Host":"*.pipe.int.trafficmanager.net","HostType":"wildcard","RelyingParty":"http://vortex-sbx.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"*.events-sandbox.data.microsoft.com","HostType":"wildcard","RelyingParty":"http://vortex-sbx.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"musicimage.xboxlive.com","HostType":"fqdn"},{"Protocol":"https","Host":"*.xboxservices.com","HostType":"wildcard","RelyingParty":"http://mp.microsoft.com/","TokenType":"JWT"},{"Protocol":"https","Host":"assets.xboxservices.com","HostType":"fqdn"},{"Protocol":"https","Host":"*.mp.microsoft.com","HostType":"wildcard","RelyingParty":"http://mp.microsoft.com/","TokenType":"JWT"},{"Protocol":"https","Host":"account.microsoft.com","HostType":"fqdn","RelyingParty":"http://mp.microsoft.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.account.microsoft.com","HostType":"wildcard","RelyingParty":"http://mp.microsoft.com/","TokenType":"JWT"},{"Protocol":"https","Host":"licensing.mp.microsoft.com","HostType":"fqdn","RelyingParty":"http://licensing.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"collections.mp.microsoft.com","HostType":"fqdn","RelyingParty":"http://licensing.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"api.twitch.tv","HostType":"fqdn","RelyingParty":"https://twitchxboxrp.twitch.tv/","TokenType":"JWT"},{"Protocol":"https","Host":"xdes.xboxlive.com","HostType":"fqdn","RelyingParty":"http://xdes.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.gameservices.xboxlive.com","HostType":"wildcard","RelyingParty":"https://gameservices.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"ssl.bing.com","HostType":"fqdn","Path":"/speechreco/xbox/accessibility","RelyingParty":"http://platform.bing.com/","TokenType":"JWT"},{"Protocol":"https","Host":"speech.bing.com","HostType":"fqdn","RelyingParty":"http://platform.bing.com/","TokenType":"JWT"},{"Protocol":"https","Host":"zto.dds.microsoft.com","HostType":"fqdn","RelyingParty":"http://dds.microsoft.com","TokenType":"JWT"},{"Protocol":"https","Host":"user.mgt.xboxlive.com","HostType":"fqdn","RelyingParty":"http://accounts.xboxlive.com","TokenType":"JWT"},{"Protocol":"https","Host":"gssv-auth-prod.xboxlive.com","HostType":"fqdn","RelyingParty":"http://gssv.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"gssv-auth-strs.xboxlive.com","HostType":"fqdn","RelyingParty":"http://gssv.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.gssv-play-prod.xboxlive.com","HostType":"wildcard","RelyingParty":"http://gssv.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.gssv-play-test.xboxlive.com","HostType":"wildcard","RelyingParty":"http://gssv.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.gssv-play-int.xboxlive.com","HostType":"wildcard","RelyingParty":"http://gssv.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"*.gssv-play-prod.xboxlive.com","HostType":"wildcard","Path":"/v5/sessions/cloud","RelyingParty":"http://connect.gssv.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.gssv-play-test.xboxlive.com","HostType":"wildcard","Path":"/v5/sessions/cloud","RelyingParty":"http://connect.gssv.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.gssv-play-int.xboxlive.com","HostType":"wildcard","Path":"/v5/sessions/cloud","RelyingParty":"http://connect.gssv.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"settings-sandbox.data.microsoft.com","HostType":"fqdn","RelyingParty":"http://onesettings-xbox-rp.com/","TokenType":"JWT"},{"Protocol":"https","Host":"settings-win.data.microsoft.com","HostType":"fqdn","RelyingParty":"http://onesettings-xbox-rp.com/","TokenType":"JWT","ServerCertIndex":[2]},{"Protocol":"https","Host":"settings-ppe.data.microsoft.com","HostType":"fqdn","RelyingParty":"http://onesettings-xbox-rp.com/","TokenType":"JWT"},{"Protocol":"https","Host":"settings.data.microsoft.com","HostType":"fqdn","RelyingParty":"http://onesettings-xbox-rp.com/","TokenType":"JWT","ServerCertIndex":[3,0]},{"Protocol":"https","Host":"playfabapi.com","HostType":"fqdn","RelyingParty":"http://playfab.xboxlive.com/","TokenType":"JWT"},{"Protocol":"https","Host":"sisu.xboxlive.com","HostType":"fqdn","RelyingParty":"http://sisu.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"beta-sisu.xboxlive.com","HostType":"fqdn","RelyingParty":"http://sisu.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.gamepass.com","HostType":"wildcard","RelyingParty":"http://xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xflight.xboxlive.com","HostType":"fqdn","RelyingParty":"http://xflight.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xrap.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://rap.xboxflight.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"rap-fd.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://rap.xboxflight.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"streaming.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://streaming.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.support.xboxlive.com","HostType":"wildcard","RelyingParty":"rp://support.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"streaming-ppe.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://streaming.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xoobe.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://xoobe.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xoobe.platform.xboxservices.com","HostType":"fqdn","RelyingParty":"rp://xoobe.xboxlive.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.ide.platform.xboxservices.com","HostType":"wildcard","RelyingParty":"rp://ide.platform.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.ppe.exp.xboxservices.com","HostType":"wildcard","RelyingParty":"rp://ppe.exp.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.exp.xboxservices.com","HostType":"wildcard","RelyingParty":"rp://exp.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"contentaccess.exp.xboxservices.com","HostType":"fqdn","RelyingParty":"http://mp.microsoft.com/","TokenType":"JWT"},{"Protocol":"https","Host":"gamingconsent-staging.xboxlive.com","HostType":"fqdn","RelyingParty":"http://gamingconsent.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"gamingconsent.xboxlive.com","HostType":"fqdn","RelyingParty":"http://gamingconsent.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"*.sucu.xboxlive.com","HostType":"wildcard","RelyingParty":"http://update.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xai.xboxservices.com","HostType":"fqdn","RelyingParty":"rp://xai.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"int.xai.xboxservices.com","HostType":"fqdn","RelyingParty":"rp://ppe.xai.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"test.xai.xboxservices.com","HostType":"fqdn","RelyingParty":"rp://ppe.xai.xboxservices.com/","TokenType":"JWT","SignaturePolicyIndex":0},{"Protocol":"https","Host":"xgrant.xboxlive.com","HostType":"fqdn","RelyingParty":"rp://rewards.xboxlive.com","TokenType":"JWT","SignaturePolicyIndex":0}],"SignaturePolicies":[{"Version":1,"SupportedAlgorithms":["ES256"],"MaxBodyBytes":8192,"SupportedSignatureTypes":["XBL"]},{"Version":1,"SupportedAlgorithms":["ES256"],"MaxBodyBytes":2147483647,"SupportedSignatureTypes":["XBL"]},{"Version":2,"SupportedAlgorithms":["ES256"],"MaxBodyBytes":0,"SupportedSignatureTypes":["DPoP","XBL"]}],"Certs":[{"Thumbprint":"54D9D20239080C32316ED9FF980A48988F4ADF2D","IsIssuer":true,"RootCertIndex":0},{"Thumbprint":"D5A9ACDB80066D0E67FF65A939BBBC952F8ED171","RootCertIndex":0},{"Thumbprint":"8F43288AD272F3103B6FB1428485EA3014C0BCFE","IsIssuer":true,"RootCertIndex":1},{"Thumbprint":"AD898AC73DF333EB60AC1F5FC6C4B2219DDB79B7","IsIssuer":true,"RootCertIndex":0}],"RootCerts":["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"]}"##; + let response = serde_json::from_str::(string).expect("Failed to parse"); + + let live = get_endpoint("https://xboxlive.com", &response).expect("No xboxlive endpoint"); + assert_eq!(live.host, "xboxlive.com"); + assert_eq!(live.relying_party.as_deref(), Some("http://xboxlive.com")); + let playfab = + get_endpoint("https://playfabapi.com", &response).expect("No playfab endpoint found"); + assert_eq!(playfab.host, "playfabapi.com"); + assert_eq!( + playfab.relying_party.as_deref(), + Some("http://playfab.xboxlive.com/") + ); + let updates = + get_endpoint("https://update.xboxlive.com", &response).expect("No updates endpoint"); + + assert_eq!(updates.host, "update.xboxlive.com"); + assert_eq!(updates.relying_party.as_deref(), Some("http://update.xboxlive.com")); + } +} diff --git a/xodus/src/lib.rs b/xodus/src/lib.rs index f964f1c..b5df17f 100644 --- a/xodus/src/lib.rs +++ b/xodus/src/lib.rs @@ -10,3 +10,11 @@ pub mod xvd; pub const XBOX_LIVE_PACKAGES_PC: &str = "https://packagespc.xboxlive.com"; pub use xal; + +pub mod proto { + pub mod xodus { + pub mod gamingservices { + include!(concat!(env!("OUT_DIR"), "/xodus.gamingservices.rs")); + } + } +} \ No newline at end of file diff --git a/xodus/src/models.rs b/xodus/src/models.rs index 726d2e0..2ee45be 100644 --- a/xodus/src/models.rs +++ b/xodus/src/models.rs @@ -6,4 +6,5 @@ pub mod live; pub mod packagespc; pub mod secrets; pub mod soap; +pub mod xbox; pub mod xvd; diff --git a/xodus/src/models/xbox.rs b/xodus/src/models/xbox.rs new file mode 100644 index 0000000..5ef7381 --- /dev/null +++ b/xodus/src/models/xbox.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UserAuthRequest { + pub relying_party: String, + pub token_type: String, + pub properties: UserAuthProperties, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UserAuthProperties { + pub auth_method: String, + pub site_name: String, + pub rps_ticket: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct XstsResponse { + pub token: String, + display_claims: DisplayClaims, +} + +#[derive(Debug, Deserialize)] +struct DisplayClaims { + xui: Vec, +} + +#[derive(Debug, Deserialize)] +struct XuiClaim { + uhs: String, +} + +impl XstsResponse { + pub fn user_hash(&self) -> Option<&str> { + self.display_claims.xui.first().map(|claim| claim.uhs.as_str()) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct XstsPropertyBag { + #[serde(skip_serializing_if = "Option::is_none")] + pub service_token: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub user_tokens: Option>, + + #[serde(rename = "SandboxId", skip_serializing_if = "Option::is_none")] + pub sandbox_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub delegation_token: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct XstsRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub relying_party: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub token_type: Option, + + pub properties: XstsPropertyBag, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct TitleMgtResponse { + pub end_points: Vec, + pub signature_policies: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct TitleMgtEndPoint { + pub protocol: String, + pub host: String, + #[serde(default)] + pub host_type: Option, + #[serde(default)] + pub relying_party: Option, + #[serde(default)] + pub token_type: Option, + #[serde(default)] + pub signature_policy_index: Option +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct TitleMgtSignaturePolicy { + pub version: u16, + pub supported_algorithms: Vec, + pub max_body_bytes: u64, + pub supported_signature_types: Vec +} \ No newline at end of file From 7e15f5cea048425987999e77da0f94eaecc4d803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Sat, 13 Jun 2026 19:11:36 +0200 Subject: [PATCH 2/6] chore: add defaul-run --- xodus-cli/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/xodus-cli/Cargo.toml b/xodus-cli/Cargo.toml index 72fa846..558bbf6 100644 --- a/xodus-cli/Cargo.toml +++ b/xodus-cli/Cargo.toml @@ -2,6 +2,7 @@ name = "xodus-cli" version = "0.1.0" edition = "2024" +default-run = "xodus-cli" [dependencies] xodus = { path = "../xodus" } From e1d0bb7d62704c25e9ff3e7baa2790c70d4b8890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Mon, 15 Jun 2026 00:11:57 +0200 Subject: [PATCH 3/6] feat: support xodus service IPC via XML - xsts token --- Cargo.lock | 11 ++ proto/xodus.gamingservices.proto | 20 ---- proto/xodus/common.proto | 11 ++ scripts/send_xsts_token_request.py | 55 ++++++++++ xodus-service/Cargo.toml | 9 ++ xodus-service/src/connection/mod.rs | 14 +++ xodus-service/src/connection/proto.rs | 8 ++ xodus-service/src/connection/router.rs | 38 +++++++ xodus-service/src/connection/xml.rs | 85 +++++++++++++++ xodus-service/src/device.rs | 124 ++++++++++++++++++++++ xodus-service/src/main.rs | 57 +++++++++- xodus-service/src/simple_context.rs | 137 +++++++++++++++++++++++++ xodus-service/src/user.rs | 16 +++ xodus-service/src/utils.rs | 9 ++ xodus/build.rs | 4 +- xodus/src/api/xbox/auth.rs | 4 +- xodus/src/api/xbox/title.rs | 9 +- xodus/src/lib.rs | 6 +- xodus/src/{models.rs => models/mod.rs} | 2 + xodus/src/models/xbox.rs | 22 ++-- xodus/src/models/xgameruntime/mod.rs | 1 + xodus/src/models/xgameruntime/xuser.rs | 34 ++++++ xodus/src/models/xodus.rs | 6 ++ xodus/src/secrets.rs | 1 - 24 files changed, 639 insertions(+), 44 deletions(-) delete mode 100644 proto/xodus.gamingservices.proto create mode 100644 proto/xodus/common.proto create mode 100755 scripts/send_xsts_token_request.py create mode 100644 xodus-service/src/connection/mod.rs create mode 100644 xodus-service/src/connection/proto.rs create mode 100644 xodus-service/src/connection/router.rs create mode 100644 xodus-service/src/connection/xml.rs create mode 100644 xodus-service/src/device.rs create mode 100644 xodus-service/src/simple_context.rs create mode 100644 xodus-service/src/user.rs create mode 100644 xodus-service/src/utils.rs rename xodus/src/{models.rs => models/mod.rs} (82%) create mode 100644 xodus/src/models/xgameruntime/mod.rs create mode 100644 xodus/src/models/xgameruntime/xuser.rs create mode 100644 xodus/src/models/xodus.rs diff --git a/Cargo.lock b/Cargo.lock index ac8b5c9..a7adf79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6123,6 +6123,17 @@ dependencies = [ [[package]] name = "xodus-service" version = "0.1.0" +dependencies = [ + "chrono", + "env_logger", + "log", + "quick-xml", + "reqwest", + "serde_json", + "tokio", + "tokio-util", + "xodus", +] [[package]] name = "yasna" diff --git a/proto/xodus.gamingservices.proto b/proto/xodus.gamingservices.proto deleted file mode 100644 index a96b098..0000000 --- a/proto/xodus.gamingservices.proto +++ /dev/null @@ -1,20 +0,0 @@ -syntax = "proto3"; - -package xodus.gamingservices; - -message XSTSTokenRequest { - string url = 1; -} - -message XSTSTokenResponse { - string token = 1; - uint64 expiry = 2; - string relying_party = 3; - optional TitleSignaturePolicy policy = 4; -} - -message TitleSignaturePolicy { - repeated string algorithms = 1; - repeated uint64 max_body_bytes = 2; - repeated string signature_types = 3; -} \ No newline at end of file diff --git a/proto/xodus/common.proto b/proto/xodus/common.proto new file mode 100644 index 0000000..876f23e --- /dev/null +++ b/proto/xodus/common.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package xodus; + +enum XodusMessageType { + UNKNOWN = 0; + PING = 1; + PONG = 2; + XSTS_TOKEN_REQUEST = 3; + XSTS_TOKEN_RESPONSE = 4; +} diff --git a/scripts/send_xsts_token_request.py b/scripts/send_xsts_token_request.py new file mode 100755 index 0000000..dc8a064 --- /dev/null +++ b/scripts/send_xsts_token_request.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import argparse +import socket +import struct +import sys +from xml.sax.saxutils import escape + + +XML_MAGIC = 0x58445358 +XSTS_TOKEN_REQUEST = 3 + + +def build_xml(url: str) -> bytes: + # Match serde PascalCase field naming in XSTSTokenRequest. + xml = f"{escape(url)}" + return xml.encode("utf-8") + + +def build_packet(payload: bytes) -> bytes: + if len(payload) > 0xFFFF: + raise ValueError("Payload too large for u16 message_size field") + + header = struct.pack(" int: + parser = argparse.ArgumentParser( + description="Send an XSTS_TOKEN_REQUEST XML message to a Unix socket" + ) + parser.add_argument("socket_path", help="Path to Unix socket (for example /run/user/1000/xodus.sock)") + parser.add_argument("url", help="Url field value for XSTSTokenRequest") + args = parser.parse_args() + + payload = build_xml(args.url) + packet = build_packet(payload) + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(args.socket_path) + print(packet) + sock.send(packet) + response = sock.recv(64 * 1024) + print("Response:", response) + except OSError as exc: + print(f"Failed to send request: {exc}", file=sys.stderr) + return 1 + + print(f"Sent {len(packet)} bytes to {args.socket_path}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/xodus-service/Cargo.toml b/xodus-service/Cargo.toml index c091d6e..5ae0774 100644 --- a/xodus-service/Cargo.toml +++ b/xodus-service/Cargo.toml @@ -4,3 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] +xodus = { path = "../xodus" } +tokio = { workspace = true } +reqwest = { workspace = true } +chrono = { workspace = true } +quick-xml = { version = "0.40.1", features = ["serialize", "overlapped-lists"] } +log = "0.4.28" +env_logger = "0.11.8" +tokio-util = "0.7.18" +serde_json = "1.0.145" diff --git a/xodus-service/src/connection/mod.rs b/xodus-service/src/connection/mod.rs new file mode 100644 index 0000000..4f5948a --- /dev/null +++ b/xodus-service/src/connection/mod.rs @@ -0,0 +1,14 @@ +pub mod proto; +pub mod router; +pub mod xml; + +pub fn encode_message(magic: u32, msg_type: u16, message_buffer: Vec) -> Vec { + let mut buffer = Vec::with_capacity(8); + let size = message_buffer.len() as u16; + buffer.extend(magic.to_le_bytes()); + buffer.extend(msg_type.to_le_bytes()); + buffer.extend(size.to_le_bytes()); + buffer.extend(message_buffer); + + buffer +} diff --git a/xodus-service/src/connection/proto.rs b/xodus-service/src/connection/proto.rs new file mode 100644 index 0000000..a14591f --- /dev/null +++ b/xodus-service/src/connection/proto.rs @@ -0,0 +1,8 @@ +use crate::simple_context::SimpleContext; + +pub async fn handle( + _socket: &mut tokio::net::UnixStream, + _context: &mut SimpleContext, +) -> tokio::io::Result<()> { + unimplemented!("Protobuf path isnt implemented yet"); +} diff --git a/xodus-service/src/connection/router.rs b/xodus-service/src/connection/router.rs new file mode 100644 index 0000000..6454cdb --- /dev/null +++ b/xodus-service/src/connection/router.rs @@ -0,0 +1,38 @@ +use tokio::io::AsyncReadExt; +use tokio_util::sync::CancellationToken; +use xodus::models::secrets::LegacyToken; + +use crate::simple_context::SimpleContext; + +pub async fn route( + (mut socket, _address): (tokio::net::UnixStream, tokio::net::unix::SocketAddr), + token: CancellationToken, + device_token: LegacyToken, +) { + let mut context = SimpleContext::new(device_token); + loop { + let mut read_magic = [0; 4]; + if token.is_cancelled() { + return; + } + let read = socket.read_exact(&mut read_magic).await; + if let Err(err) = read { + log::error!("Failed to read magic: {err:?}"); + return; + } + + let magic = u32::from_le_bytes(read_magic); + let res = match magic { + crate::XML_MAGIC => super::xml::handle(&mut socket, &mut context).await, + crate::PROTO_MAGIC => super::proto::handle(&mut socket, &mut context).await, + _ => { + log::error!("Unknown magic"); + return; + } + }; + + if let Err(err) = res { + log::error!("There was an error handling the message: {err}"); + } + } +} diff --git a/xodus-service/src/connection/xml.rs b/xodus-service/src/connection/xml.rs new file mode 100644 index 0000000..2197d54 --- /dev/null +++ b/xodus-service/src/connection/xml.rs @@ -0,0 +1,85 @@ +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use xodus::{ + models::xgameruntime::xuser::{ + TitleSignatureAlgorithms, TitleSignaturePolicy, TitleSignatureTypes, XSTSTokenRequest, + XSTSTokenResponse, + }, + proto::xodus::XodusMessageType, +}; + +use crate::{XML_MAGIC, simple_context::SimpleContext}; + +pub async fn handle( + socket: &mut tokio::net::UnixStream, + context: &mut SimpleContext, +) -> tokio::io::Result<()> { + log::debug!("Parsing XML"); + let message_type = socket.read_u16_le().await?; + let message_size = socket.read_u16_le().await?; + let mut buffer = vec![0; message_size as usize]; + log::debug!("Reading buffer {message_size}"); + socket.read_exact(&mut buffer).await?; + log::debug!("Read buffer"); + let message_type = XodusMessageType::try_from(message_type as i32).unwrap_or_default(); + + let out_buf = match parse_message(context, message_type, buffer).await { + Ok(buf) => buf, + Err(err) => { + log::error!("Failed parsing message: {err}"); + vec![] + } + }; + + let data = super::encode_message(XML_MAGIC, message_type as u16, out_buf); + socket.write_all(&data).await +} + +pub async fn parse_message( + context: &mut SimpleContext, + message_type: XodusMessageType, + buffer: Vec, +) -> Result, Box> { + match message_type { + XodusMessageType::XstsTokenRequest => { + let string_buf = std::str::from_utf8(&buffer)?; + let req = quick_xml::de::from_str::(string_buf)?; + log::debug!("Getting title config {}", req.url); + let (title_cfg, policy) = context + .get_title_config(&req.url) + .await + .ok_or::("Failed to get title cfg".into())?; + log::debug!("Got title config for {}", title_cfg.host); + let relying_party = title_cfg + .relying_party + .unwrap_or("http://xboxlive.com".to_string()); + log::debug!("Getting token {relying_party}"); + let user_token = context + .get_token(&relying_party) + .await + .ok_or::("Failed to get user cfg".into())?; + + let payload = XSTSTokenResponse { + token: format!( + "XBL3.0 x={};{}", + user_token.user_hash().unwrap(), + user_token.token + ), + expiry: user_token.not_after.timestamp(), + relying_party, + signature_policy: TitleSignaturePolicy { + algorithms: TitleSignatureAlgorithms { + algorithm: policy.supported_algorithms, + }, + signature_types: TitleSignatureTypes { + signature: policy.supported_signature_types, + }, + max_body_bytes: policy.max_body_bytes, + }, + }; + + let payload = quick_xml::se::to_string(&payload)?; + Ok(payload.as_bytes().to_vec()) + } + _ => Err("Unimplemented".into()), + } +} diff --git a/xodus-service/src/device.rs b/xodus-service/src/device.rs new file mode 100644 index 0000000..908099a --- /dev/null +++ b/xodus-service/src/device.rs @@ -0,0 +1,124 @@ +use std::collections::HashMap; + +use xodus::{ + hardware, + licensing::utils::generate_string, + models::{ + devicecredential::{Authentication, ClientInfo, DeviceAddRequest, DeviceInfo}, + secrets::{Token, TokenStore}, + soap::BodyContent, + }, +}; + +pub async fn ensure_device_credentials(client: &reqwest::Client) { + let license = get_dev_license(); + if license.is_err() { + let username = format!("02{}", generate_string(14)); + let password = generate_string(20); + let provision = DeviceAddRequest { + client_info: ClientInfo::default(), + authentication: Authentication::new(username.clone(), password.clone()), + device_info: Some(DeviceInfo { + id: "DeviceInfo".to_string(), + components: hardware::probe_provision_components(), + }), + }; + + let dev = xodus::api::live::login_device_credential(client, provision) + .await + .expect("Failed to get device creds"); + + let device = xodus::models::secrets::Device { + username: username.clone(), + password: password.clone(), + puid: dev.puid, + hwid: dev.hw_device_id, + device_id: dev.license.binding.device_id.unwrap_or_default(), + splicense: dev.license.splicense_block, + }; + + let entry = xodus::secrets::get_entry("dev_license").unwrap(); + let json = serde_json::to_string(&device).unwrap(); + entry.set_secret(json.as_bytes()).unwrap(); + + let tokens = xodus::api::live::authenticate_device(client, username, password) + .await + .expect("Failed to auth device"); + + if let BodyContent::RequestSecurityTokenResponse(resp) = tokens.body.body { + let key_name = resp + .requested_security_token + .encrypted_data + .as_ref() + .unwrap() + .key_info + .key_name + .as_ref() + .unwrap(); + let key_name = key_name.clone(); + let token: xodus::models::secrets::Token = resp.into(); + save_token(key_name, token); + } + } else if get_device_token().is_err() { + let license = license.unwrap(); + let tokens = + xodus::api::live::authenticate_device(client, license.username, license.password) + .await + .expect("Failed to auth device"); + + if let BodyContent::RequestSecurityTokenResponse(resp) = tokens.body.body { + let key_name = resp + .requested_security_token + .encrypted_data + .as_ref() + .unwrap() + .key_info + .key_name + .as_ref() + .unwrap(); + let key_name = key_name.clone(); + let token: xodus::models::secrets::Token = resp.into(); + save_token(key_name, token); + } + } +} + +pub fn get_dev_license() -> Result> { + let device_entry = xodus::secrets::get_entry("dev_license")?; + let secret = device_entry.get_secret()?; + let dev = serde_json::from_slice::(secret.as_slice())?; + Ok(dev) +} + +pub fn get_device_token() -> Result> { + get_token("http://Passport.NET/STS".to_string()).ok_or("Error".into()) +} + +pub fn save_token(address: String, token: Token) { + let entry = xodus::secrets::get_entry("device-tokens").unwrap(); + let passwd = entry.get_password().unwrap_or_default(); + + let mut tokens = if !passwd.is_empty() { + let tokens = serde_json::from_str::(&passwd).unwrap(); + tokens.tokens + } else { + HashMap::new() + }; + tokens.insert(address, token); + let tokens = TokenStore { tokens }; + let tokens_str = serde_json::to_string(&tokens).unwrap(); + entry.set_password(&tokens_str).unwrap(); +} + +pub fn get_token(address: String) -> Option { + let entry = xodus::secrets::get_entry("device-tokens").unwrap(); + let passwd = entry.get_password().unwrap_or_default(); + + let tokens = if !passwd.is_empty() { + let tokens = serde_json::from_str::(&passwd).unwrap(); + tokens.tokens + } else { + HashMap::new() + }; + tokens.get(&address).cloned() +} diff --git a/xodus-service/src/main.rs b/xodus-service/src/main.rs index e7a11a9..49427e4 100644 --- a/xodus-service/src/main.rs +++ b/xodus-service/src/main.rs @@ -1,3 +1,56 @@ -fn main() { - println!("Hello, world!"); +use std::{fs::Permissions, os::unix::fs::PermissionsExt}; + +use tokio::net::UnixListener; +use tokio_util::sync::CancellationToken; + +mod connection; +mod device; +mod simple_context; +mod user; +mod utils; + +const XML_MAGIC: u32 = 0x58445358; +const PROTO_MAGIC: u32 = 0x58445350; + +#[tokio::main] +async fn main() { + xodus::secrets::init_secrets().expect("Failed to init keychain"); + device::ensure_device_credentials(&reqwest::Client::new()).await; + let xodus::models::secrets::Token::Legacy(device_token) = device::get_device_token().unwrap() + else { + panic!("Device token isnt legacy") + }; + + env_logger::init_from_env("XODUS_LOG"); + let runtime_dir = utils::get_runtime_dir(); + let cancellation = CancellationToken::new(); + let socket_path = format!("{runtime_dir}/xodus.sock"); + let trigger = cancellation.clone(); + tokio::spawn(async move { + tokio::signal::ctrl_c() + .await + .expect("Failure to handle ctrl_c"); + trigger.cancel(); + }); + { + let listener = UnixListener::bind(&socket_path).expect("Unable to bind to socket"); + let mode = 0o600; + let perms = Permissions::from_mode(mode); + _ = tokio::fs::set_permissions(&socket_path, perms).await; + loop { + let accept = tokio::select! { + r = listener.accept() => r, + _ = cancellation.cancelled() => break, + } + .expect("Failed to accept"); + + let token = cancellation.clone(); + let device_token = device_token.clone(); + tokio::spawn( + async move { connection::router::route(accept, token, device_token).await }, + ); + } + } + + _ = tokio::fs::remove_file(socket_path).await; } diff --git a/xodus-service/src/simple_context.rs b/xodus-service/src/simple_context.rs new file mode 100644 index 0000000..8d0cb26 --- /dev/null +++ b/xodus-service/src/simple_context.rs @@ -0,0 +1,137 @@ +// Temporary context, full service design will be much more extensive + +use std::collections::HashMap; + +use xodus::models::{ + live::ExchangeUserTokenOutcome, + secrets::{LegacyToken, Token}, + soap, + xbox::{TitleMgtEndPoint, TitleMgtSignaturePolicy, XstsResponse}, +}; + +#[derive(Debug)] +pub struct SimpleContext { + pub client: reqwest::Client, + device_token: Option, + user_token: Option, + cached_endpoints: Option, + user_token_cache: HashMap, +} + +impl SimpleContext { + pub fn new(device_token: LegacyToken) -> Self { + let client = reqwest::ClientBuilder::new() + .user_agent(format!("xodus-service/{}", env!("CARGO_PKG_VERSION"))) + .connection_verbose(true) + .build() + .unwrap(); + + Self { + client, + device_token: Some(device_token), + user_token: None, + cached_endpoints: None, + user_token_cache: HashMap::default(), + } + } + + pub async fn get_title_config( + &mut self, + url: &str, + ) -> Option<(TitleMgtEndPoint, TitleMgtSignaturePolicy)> { + if let Some(endpoints) = &self.cached_endpoints { + let endpoint = xodus::api::xbox::title::get_endpoint(url, endpoints)?; + let policy = endpoints + .signature_policies + .get(endpoint.signature_policy_index.unwrap_or_default() as usize)?; + return Some((endpoint.clone(), policy.clone())); + } + + let title_management = xodus::api::xbox::title::get_title_management(&self.client) + .await + .ok()?; + let endpoint = xodus::api::xbox::title::get_endpoint(url, &title_management).cloned(); + let policies = title_management.signature_policies.clone(); + self.cached_endpoints = Some(title_management); + + let endpoint = endpoint?; + let policy = policies + .get(endpoint.signature_policy_index.unwrap_or_default() as usize) + .cloned()?; + + Some((endpoint, policy)) + } + + pub async fn get_token<'a>(&'a mut self, relying_party: &str) -> Option { + if let Some(token) = self.user_token_cache.get(relying_party) + && token.not_after > chrono::Utc::now() + { + return Some(token.clone()); + } + + let user_token = match self.user_token.clone() { + Some(t) => t, + None => self.get_user_token().await?, + }; + + let token = xodus::api::xbox::request_xsts_token( + &self.client, + user_token.token.clone(), + relying_party, + ) + .await + .ok()?; + self.user_token_cache + .insert(relying_party.to_string(), token.clone()); + Some(token) + } + + async fn get_user_token<'a>(&'a mut self) -> Option { + let Token::Legacy(token) = crate::user::get_token("http://Passport.NET/STS")? else { + return None; + }; + let device_token = self.device_token.as_ref().unwrap(); + let user_token = xodus::api::live::exchange_user_token( + &self.client, + token.token, + "USERNAME".to_string(), + device_token.token.clone(), + device_token.binary_secret.clone().unwrap(), + None, + Some("Silent".to_string()), + "{d6d5a677-0872-4ab0-9442-bb792fce85c5}".to_string(), + &[( + "user.auth.xboxlive.com".to_owned(), + Some(soap::PolicyReference::mbi_ssl()), + )], + ) + .await + .ok()?; + + let user_token: Token = match user_token { + ExchangeUserTokenOutcome::Fault(_) => { + eprintln!("Failed to get exchange MS token"); + panic!("TODO"); + } + ExchangeUserTokenOutcome::Issued( + soap::BodyContent::RequestSecurityTokenResponseCollection(mut collection), + ) => { + let token = collection.security_tokens.remove(0); + token.into() + } + ExchangeUserTokenOutcome::Issued(soap::BodyContent::RequestSecurityTokenResponse( + token, + )) => token.into(), + _ => unreachable!("Only responses are handled"), + }; + let Token::Compact(user_token) = user_token else { + return None; + }; + + let resp = xodus::api::xbox::authenticate_xbox_user(&self.client, user_token) + .await + .ok()?; + self.user_token = Some(resp.clone()); + Some(resp) + } +} diff --git a/xodus-service/src/user.rs b/xodus-service/src/user.rs new file mode 100644 index 0000000..6829d0e --- /dev/null +++ b/xodus-service/src/user.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use xodus::models::secrets::{Token, TokenStore}; + +pub fn get_token(address: &str) -> Option { + let entry = xodus::secrets::get_entry("user-tokens").unwrap(); + let passwd = entry.get_password().unwrap_or_default(); + + let tokens = if !passwd.is_empty() { + let tokens = serde_json::from_str::(&passwd).unwrap(); + tokens.tokens + } else { + HashMap::new() + }; + tokens.get(address).cloned() +} diff --git a/xodus-service/src/utils.rs b/xodus-service/src/utils.rs new file mode 100644 index 0000000..ce18e07 --- /dev/null +++ b/xodus-service/src/utils.rs @@ -0,0 +1,9 @@ +#[cfg(target_os = "linux")] +pub fn get_runtime_dir() -> String { + std::env::var("XDG_RUNTIME_DIR").expect("Runtime dir not set") +} + +#[cfg(target_os = "macos")] +pub fn get_runtime_dir() -> String { + return "/tmp"; +} diff --git a/xodus/build.rs b/xodus/build.rs index a4ecdb7..4ac003b 100644 --- a/xodus/build.rs +++ b/xodus/build.rs @@ -1,5 +1,5 @@ use std::io::Result; fn main() -> Result<()> { - prost_build::compile_protos(&["../proto/xodus.gamingservices.proto"], &["../proto"])?; + prost_build::compile_protos(&["../proto/xodus/common.proto"], &["../proto"])?; Ok(()) -} \ No newline at end of file +} diff --git a/xodus/src/api/xbox/auth.rs b/xodus/src/api/xbox/auth.rs index 483f123..bf2aa0e 100644 --- a/xodus/src/api/xbox/auth.rs +++ b/xodus/src/api/xbox/auth.rs @@ -57,8 +57,6 @@ pub async fn request_xsts_token( } pub fn get_xsts_auth_header(xsts: XstsResponse) -> String { - let uhs = xsts - .user_hash() - .expect("XSTS response missing xui claim"); + let uhs = xsts.user_hash().expect("XSTS response missing xui claim"); format!("XBL3.0 x={uhs};{}", xsts.token) } diff --git a/xodus/src/api/xbox/title.rs b/xodus/src/api/xbox/title.rs index 6dd18d5..434df33 100644 --- a/xodus/src/api/xbox/title.rs +++ b/xodus/src/api/xbox/title.rs @@ -22,7 +22,7 @@ pub fn get_endpoint<'a>(url: &str, response: &'a TitleMgtResponse) -> Option<&'a let set = globset::GlobSet::new(hosts).ok()?; let matched = set.matches(parsed_url.host_str().unwrap()); let matched: Vec<&TitleMgtEndPoint> = matched.into_iter().map(|m| filtered[m]).collect(); - println!("Matched: {matched:?}"); + let index = matched .iter() .max_by_key(|&&pat| pat.host.chars().filter(|&c| c != '*').count()) @@ -52,8 +52,11 @@ mod test { ); let updates = get_endpoint("https://update.xboxlive.com", &response).expect("No updates endpoint"); - + assert_eq!(updates.host, "update.xboxlive.com"); - assert_eq!(updates.relying_party.as_deref(), Some("http://update.xboxlive.com")); + assert_eq!( + updates.relying_party.as_deref(), + Some("http://update.xboxlive.com") + ); } } diff --git a/xodus/src/lib.rs b/xodus/src/lib.rs index b5df17f..38e5f95 100644 --- a/xodus/src/lib.rs +++ b/xodus/src/lib.rs @@ -13,8 +13,6 @@ pub use xal; pub mod proto { pub mod xodus { - pub mod gamingservices { - include!(concat!(env!("OUT_DIR"), "/xodus.gamingservices.rs")); - } + include!(concat!(env!("OUT_DIR"), "/xodus.rs")); } -} \ No newline at end of file +} diff --git a/xodus/src/models.rs b/xodus/src/models/mod.rs similarity index 82% rename from xodus/src/models.rs rename to xodus/src/models/mod.rs index 2ee45be..d5fd168 100644 --- a/xodus/src/models.rs +++ b/xodus/src/models/mod.rs @@ -7,4 +7,6 @@ pub mod packagespc; pub mod secrets; pub mod soap; pub mod xbox; +pub mod xgameruntime; +pub mod xodus; pub mod xvd; diff --git a/xodus/src/models/xbox.rs b/xodus/src/models/xbox.rs index 5ef7381..688f2fe 100644 --- a/xodus/src/models/xbox.rs +++ b/xodus/src/models/xbox.rs @@ -16,26 +16,30 @@ pub struct UserAuthProperties { pub rps_ticket: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] pub struct XstsResponse { + pub not_after: chrono::DateTime, pub token: String, display_claims: DisplayClaims, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] struct DisplayClaims { xui: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] struct XuiClaim { uhs: String, } impl XstsResponse { pub fn user_hash(&self) -> Option<&str> { - self.display_claims.xui.first().map(|claim| claim.uhs.as_str()) + self.display_claims + .xui + .first() + .map(|claim| claim.uhs.as_str()) } } @@ -74,7 +78,7 @@ pub struct TitleMgtResponse { pub signature_policies: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] pub struct TitleMgtEndPoint { pub protocol: String, @@ -86,14 +90,14 @@ pub struct TitleMgtEndPoint { #[serde(default)] pub token_type: Option, #[serde(default)] - pub signature_policy_index: Option + pub signature_policy_index: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] pub struct TitleMgtSignaturePolicy { pub version: u16, pub supported_algorithms: Vec, pub max_body_bytes: u64, - pub supported_signature_types: Vec -} \ No newline at end of file + pub supported_signature_types: Vec, +} diff --git a/xodus/src/models/xgameruntime/mod.rs b/xodus/src/models/xgameruntime/mod.rs new file mode 100644 index 0000000..01f38b7 --- /dev/null +++ b/xodus/src/models/xgameruntime/mod.rs @@ -0,0 +1 @@ +pub mod xuser; diff --git a/xodus/src/models/xgameruntime/xuser.rs b/xodus/src/models/xgameruntime/xuser.rs new file mode 100644 index 0000000..97d961f --- /dev/null +++ b/xodus/src/models/xgameruntime/xuser.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct XSTSTokenRequest { + pub url: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct XSTSTokenResponse { + pub token: String, + pub expiry: i64, + pub relying_party: String, + pub signature_policy: TitleSignaturePolicy, +} + +#[derive(Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct TitleSignaturePolicy { + pub algorithms: TitleSignatureAlgorithms, + pub max_body_bytes: u64, + pub signature_types: TitleSignatureTypes, +} + +#[derive(Serialize)] +pub struct TitleSignatureAlgorithms { + pub algorithm: Vec, +} + +#[derive(Serialize)] +pub struct TitleSignatureTypes { + pub signature: Vec, +} diff --git a/xodus/src/models/xodus.rs b/xodus/src/models/xodus.rs new file mode 100644 index 0000000..2ef328a --- /dev/null +++ b/xodus/src/models/xodus.rs @@ -0,0 +1,6 @@ +pub struct XodusIPCPacket { + pub magic: u32, + pub message_type: u16, + pub message_size: u16, + pub buffer: Vec, +} diff --git a/xodus/src/secrets.rs b/xodus/src/secrets.rs index 1b84088..7d8c229 100644 --- a/xodus/src/secrets.rs +++ b/xodus/src/secrets.rs @@ -1,4 +1,3 @@ - pub static SERVICE_NAME: &str = "Xodus Service"; pub fn init_secrets() -> Result<(), keyring_core::Error> { From 651795cb5c76c1b627f8b0cd199d0acc6950e85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Mon, 15 Jun 2026 00:15:43 +0200 Subject: [PATCH 4/6] feat: add ping message handling --- xodus-service/src/connection/xml.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xodus-service/src/connection/xml.rs b/xodus-service/src/connection/xml.rs index 2197d54..c155ece 100644 --- a/xodus-service/src/connection/xml.rs +++ b/xodus-service/src/connection/xml.rs @@ -40,6 +40,9 @@ pub async fn parse_message( buffer: Vec, ) -> Result, Box> { match message_type { + XodusMessageType::Ping => { + Ok(buffer) + } XodusMessageType::XstsTokenRequest => { let string_buf = std::str::from_utf8(&buffer)?; let req = quick_xml::de::from_str::(string_buf)?; From 45092924dba9bd272691fd9718a9a916581563e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Mon, 15 Jun 2026 00:25:23 +0200 Subject: [PATCH 5/6] fix: mac build issue --- xodus-service/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xodus-service/src/utils.rs b/xodus-service/src/utils.rs index ce18e07..1cd44e8 100644 --- a/xodus-service/src/utils.rs +++ b/xodus-service/src/utils.rs @@ -5,5 +5,5 @@ pub fn get_runtime_dir() -> String { #[cfg(target_os = "macos")] pub fn get_runtime_dir() -> String { - return "/tmp"; + return "/tmp/".to_string(); } From d6d7e9bee6a95d6c35f9f549fb9fad597af739d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lidwin?= Date: Mon, 15 Jun 2026 02:09:24 +0200 Subject: [PATCH 6/6] fix: send proper message type in response --- xodus-service/src/connection/xml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xodus-service/src/connection/xml.rs b/xodus-service/src/connection/xml.rs index c155ece..9d0e150 100644 --- a/xodus-service/src/connection/xml.rs +++ b/xodus-service/src/connection/xml.rs @@ -30,7 +30,7 @@ pub async fn handle( } }; - let data = super::encode_message(XML_MAGIC, message_type as u16, out_buf); + let data = super::encode_message(XML_MAGIC, message_type as u16 + 1, out_buf); socket.write_all(&data).await }