242 lines
8.2 KiB
Rust
242 lines
8.2 KiB
Rust
//! Exemple de Spoofer Qobuz - Extraction dynamique des AppID et secrets
|
|
//!
|
|
//! Cet exemple reproduit le comportement du spoofer Python :
|
|
//! 1. Récupère la page de login Qobuz
|
|
//! 2. Extrait l'URL du bundle.js
|
|
//! 3. Télécharge le bundle
|
|
//! 4. Extrait l'AppID et les secrets via regex
|
|
//! 5. Décode les secrets en base64
|
|
//!
|
|
//! Usage:
|
|
//! ```bash
|
|
//! cargo run --example spoofer
|
|
//! ```
|
|
|
|
use anyhow::Result;
|
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
use indexmap::IndexMap;
|
|
use regex::Regex;
|
|
use reqwest::Client;
|
|
|
|
struct Spoofer {
|
|
bundle: String,
|
|
seed_timezone_regex: Regex,
|
|
info_extras_regex_template: String,
|
|
app_id_regex: Regex,
|
|
}
|
|
|
|
impl Spoofer {
|
|
/// Crée un nouveau Spoofer et télécharge le bundle.js
|
|
async fn new() -> Result<Self> {
|
|
// Expressions régulières (équivalent Python)
|
|
let seed_timezone_regex = Regex::new(
|
|
r#"[a-z]\.initialSeed\("(?P<seed>[\w=]+)",window\.utimezone\.(?P<timezone>[a-z]+)\)"#,
|
|
)?;
|
|
|
|
let info_extras_regex_template =
|
|
r#"name:"\w+/(?P<timezone>{timezones})",info:"(?P<info>[\w=]+)",extras:"(?P<extras>[\w=]+)""#
|
|
.to_string();
|
|
|
|
let app_id_regex = Regex::new(
|
|
r#"production:\{api:\{appId:"(?P<app_id>\d{9})",appSecret:"(?P<secret>\w{32})"\},braze:.\(.\(\{\},.\),\{\},\{apiKey:"([-0-9a-fA-F]{36})"\}\),extra:.\}"#,
|
|
)?;
|
|
|
|
// Créer un client HTTP
|
|
let client = Client::builder()
|
|
.user_agent("Mozilla/5.0 (compatible; PMOMusic/1.0)")
|
|
.build()?;
|
|
|
|
println!("Récupération de la page de login...");
|
|
let login_page = client
|
|
.get("https://play.qobuz.com/login")
|
|
.send()
|
|
.await?
|
|
.text()
|
|
.await?;
|
|
|
|
// Extraire l'URL du bundle
|
|
let bundle_url_regex =
|
|
Regex::new(r#"<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>"#)?;
|
|
let bundle_url = bundle_url_regex
|
|
.captures(&login_page)
|
|
.and_then(|cap| cap.get(1))
|
|
.ok_or_else(|| anyhow::anyhow!("Impossible de trouver l'URL du bundle"))?
|
|
.as_str();
|
|
|
|
println!("Téléchargement du bundle depuis: {}", bundle_url);
|
|
let bundle_full_url = format!("https://play.qobuz.com{}", bundle_url);
|
|
let bundle = client.get(&bundle_full_url).send().await?.text().await?;
|
|
|
|
println!("Bundle téléchargé ({} bytes)", bundle.len());
|
|
|
|
Ok(Self {
|
|
bundle,
|
|
seed_timezone_regex,
|
|
info_extras_regex_template,
|
|
app_id_regex,
|
|
})
|
|
}
|
|
|
|
/// Extrait l'App ID depuis le bundle
|
|
fn get_app_id(&self) -> Result<String> {
|
|
let captures = self
|
|
.app_id_regex
|
|
.captures(&self.bundle)
|
|
.ok_or_else(|| anyhow::anyhow!("AppID non trouvé dans le bundle"))?;
|
|
|
|
Ok(captures
|
|
.name("app_id")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe app_id non trouvé"))?
|
|
.as_str()
|
|
.to_string())
|
|
}
|
|
|
|
/// Extrait les secrets depuis le bundle
|
|
fn get_secrets(&self) -> Result<IndexMap<String, String>> {
|
|
// Étape 1: Extraire tous les seed/timezone pairs
|
|
let mut secrets: IndexMap<String, Vec<String>> = IndexMap::new();
|
|
|
|
for captures in self.seed_timezone_regex.captures_iter(&self.bundle) {
|
|
let seed = captures
|
|
.name("seed")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe seed non trouvé"))?
|
|
.as_str();
|
|
let timezone = captures
|
|
.name("timezone")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe timezone non trouvé"))?
|
|
.as_str();
|
|
|
|
secrets
|
|
.entry(timezone.to_string())
|
|
.or_insert_with(Vec::new)
|
|
.push(seed.to_string());
|
|
}
|
|
|
|
println!("Timezones trouvées: {:?}", secrets.keys());
|
|
|
|
// Étape 2: Réordonner - on met la deuxième timezone en premier
|
|
// (comme le fait le code Python avec move_to_end)
|
|
if secrets.len() >= 2 {
|
|
let keys: Vec<String> = secrets.keys().cloned().collect();
|
|
let second_key = keys[1].clone();
|
|
let second_value = secrets.get(&second_key).unwrap().clone();
|
|
|
|
// Retirer et réinsérer pour le mettre en premier
|
|
secrets.shift_remove(&second_key);
|
|
let mut new_secrets = IndexMap::new();
|
|
new_secrets.insert(second_key, second_value);
|
|
for (k, v) in secrets {
|
|
new_secrets.insert(k, v);
|
|
}
|
|
secrets = new_secrets;
|
|
}
|
|
|
|
// Étape 3: Construire la regex pour info/extras
|
|
let timezones_capitalized: Vec<String> = secrets
|
|
.keys()
|
|
.map(|tz| {
|
|
let mut chars = tz.chars();
|
|
match chars.next() {
|
|
None => String::new(),
|
|
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let info_extras_regex_str = self
|
|
.info_extras_regex_template
|
|
.replace("{timezones}", &timezones_capitalized.join("|"));
|
|
|
|
let info_extras_regex = Regex::new(&info_extras_regex_str)?;
|
|
|
|
// Étape 4: Extraire info et extras pour chaque timezone
|
|
for captures in info_extras_regex.captures_iter(&self.bundle) {
|
|
let timezone_cap = captures
|
|
.name("timezone")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe timezone non trouvé"))?
|
|
.as_str();
|
|
let info = captures
|
|
.name("info")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe info non trouvé"))?
|
|
.as_str();
|
|
let extras = captures
|
|
.name("extras")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe extras non trouvé"))?
|
|
.as_str();
|
|
|
|
let timezone_lower = timezone_cap.to_lowercase();
|
|
if let Some(vec) = secrets.get_mut(&timezone_lower) {
|
|
vec.push(info.to_string());
|
|
vec.push(extras.to_string());
|
|
}
|
|
}
|
|
|
|
// Étape 5: Décoder les secrets en base64
|
|
let mut decoded_secrets = IndexMap::new();
|
|
for (timezone, parts) in secrets {
|
|
let concatenated = parts.join("");
|
|
|
|
// Retirer les 44 derniers caractères (comme Python [:-44])
|
|
if concatenated.len() > 44 {
|
|
let trimmed = &concatenated[..concatenated.len() - 44];
|
|
|
|
// Décoder en base64
|
|
match STANDARD.decode(trimmed) {
|
|
Ok(decoded_bytes) => {
|
|
match String::from_utf8(decoded_bytes) {
|
|
Ok(decoded_str) => {
|
|
decoded_secrets.insert(timezone, decoded_str);
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Erreur UTF-8 pour timezone {}: {}",
|
|
timezone, e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Erreur de décodage base64 pour timezone {}: {}",
|
|
timezone, e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(decoded_secrets)
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Initialiser le logging
|
|
tracing_subscriber::fmt::init();
|
|
|
|
println!("=== Spoofer Qobuz ===\n");
|
|
|
|
// Créer le spoofer
|
|
let spoofer = Spoofer::new().await?;
|
|
|
|
// Extraire l'App ID
|
|
println!("\n--- App ID ---");
|
|
match spoofer.get_app_id() {
|
|
Ok(app_id) => println!("App ID: {}", app_id),
|
|
Err(e) => eprintln!("Erreur lors de l'extraction de l'App ID: {}", e),
|
|
}
|
|
|
|
// Extraire les secrets
|
|
println!("\n--- Secrets ---");
|
|
match spoofer.get_secrets() {
|
|
Ok(secrets) => {
|
|
for (timezone, secret) in secrets {
|
|
println!("{}: {}", timezone, secret);
|
|
}
|
|
}
|
|
Err(e) => eprintln!("Erreur lors de l'extraction des secrets: {}", e),
|
|
}
|
|
|
|
Ok(())
|
|
}
|