From b85b9893a8c3c8666df91ab4ae5c83a0a82faad2 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 25 Jul 2022 12:30:16 +0200 Subject: [PATCH] add client module --- Cargo.toml | 8 +- notes/desktopMusic-request.json | 86 +++++++++ notes/desktopMusic_search.json | 90 ++++++++++ src/client/mod.rs | 303 ++++++++++++++++++++++++++++++++ src/deobfuscate.rs | 56 +++--- src/lib.rs | 8 +- src/util.rs | 28 +++ 7 files changed, 550 insertions(+), 29 deletions(-) create mode 100644 notes/desktopMusic-request.json create mode 100644 notes/desktopMusic_search.json create mode 100644 src/client/mod.rs create mode 100644 src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 8f86cff..cb25787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] quick-js = "0.4.1" once_cell = "1.12.0" -regex = "1.6.0" fancy-regex = "0.10.0" anyhow = "1.0" thiserror = "1.0.31" @@ -16,3 +15,10 @@ url = "2.2.2" log = "0.4.17" reqwest = "0.11.11" tokio = {version = "1.20.0", features = ["macros"]} +serde_json = "1.0.82" +serde = { version = "1.0", features = ["derive"] } +rand = "0.8.5" + +[dev-dependencies] +env_logger = "0.9.0" +test-log = "0.2.11" diff --git a/notes/desktopMusic-request.json b/notes/desktopMusic-request.json new file mode 100644 index 0000000..06e3485 --- /dev/null +++ b/notes/desktopMusic-request.json @@ -0,0 +1,86 @@ +{ + "videoId": "a1IuJLebHgM", + "context": { + "client": { + "hl": "de", + "gl": "DE", + "remoteHost": "2003:de:af47:2200:1f01:5e28:c1a1:55b9", + "deviceMake": "", + "deviceModel": "", + "visitorData": "CgtfZGdNenp4Z1JVRSjItfSWBg%3D%3D", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36,gzip(gfe)", + "clientName": "WEB_REMIX", + "clientVersion": "1.20220715.04.00", + "osName": "X11", + "osVersion": "", + "originalUrl": "https://music.youtube.com/?cbrd=1", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "configInfo": { + "appInstallData": "CMi19JYGENSDrgUQxPb9EhCurK4FEP24_RIQlK-uBRC3y60FENCtrgUQ5vj9EhDLoq4FELiLrgUQpvP9EhDYvq0F" + }, + "browserName": "Chrome", + "browserVersion": "103.0.5060.134", + "screenWidthPoints": 540, + "screenHeightPoints": 1003, + "screenPixelDensity": 1, + "screenDensityFloat": 1, + "utcOffsetMinutes": 120, + "userInterfaceTheme": "USER_INTERFACE_THEME_DARK", + "timeZone": "Europe/Berlin", + "playerType": "UNIPLAYER", + "tvAppInfo": { "livingRoomAppMode": "LIVING_ROOM_APP_MODE_UNSPECIFIED" }, + "clientScreen": "WATCH_FULL_SCREEN" + }, + "user": { "lockedSafetyMode": false }, + "request": { + "useSsl": true, + "internalExperimentFlags": [], + "consistencyTokenJars": [] + }, + "clientScreenNonce": "MC41MjQzMjkwMzQ5NzQ3ODE4", + "adSignalsInfo": { + "params": [ + { "key": "dt", "value": "1658657482350" }, + { "key": "flash", "value": "0" }, + { "key": "frm", "value": "0" }, + { "key": "u_tz", "value": "120" }, + { "key": "u_his", "value": "3" }, + { "key": "u_h", "value": "1080" }, + { "key": "u_w", "value": "2560" }, + { "key": "u_ah", "value": "1080" }, + { "key": "u_aw", "value": "2560" }, + { "key": "u_cd", "value": "24" }, + { "key": "bc", "value": "31" }, + { "key": "bih", "value": "1003" }, + { "key": "biw", "value": "528" }, + { + "key": "brdim", + "value": "1219,-39,1219,-39,2560,0,1336,1158,540,1003" + }, + { "key": "vis", "value": "1" }, + { "key": "wgl", "value": "true" }, + { "key": "ca_type", "value": "image" } + ] + }, + "clickTracking": { + "clickTrackingParams": "CIwFEMjeAiITCKTV48-kkfkCFfrfEQgdbKQFeQ==" + } + }, + "playbackContext": { + "contentPlaybackContext": { + "html5Preference": "HTML5_PREF_WANTS", + "lactMilliseconds": "11", + "referer": "https://music.youtube.com/", + "signatureTimestamp": 19194, + "autoCaptionsDefaultOn": false, + "mdxContext": {} + } + }, + "cpn": "Hhji0Eo5WqdxXyj8", + "playlistId": "RDAMVMa1IuJLebHgM", + "captionParams": {}, + "serviceIntegrityDimensions": { + "poToken": "GpsBCm41_Q8Gdg5ubTfFjg5iom_5EMgKAKggyajvdSFL8McTI6M-exELW1WeGmPtwON1sqmJItk-wHF1WobX8rZRx49vefmv5NprWecvSx3LPl8ESu-QQZOK9VI-q9R3OdcTpJWLkcY7eEKuyItFf6k0ZxIpAX04kIiBQUQPO8JcQv_U5siKZaug6kUSg9OqLmFc7rRClBhbWq7yF1U=" + } +} diff --git a/notes/desktopMusic_search.json b/notes/desktopMusic_search.json new file mode 100644 index 0000000..7c7b92a --- /dev/null +++ b/notes/desktopMusic_search.json @@ -0,0 +1,90 @@ +{ + "context": { + "client": { + "hl": "de", + "gl": "DE", + "remoteHost": "2003:de:af47:2200:1f01:5e28:c1a1:55b9", + "deviceMake": "", + "deviceModel": "", + "visitorData": "CgtfZGdNenp4Z1JVRSjItfSWBg%3D%3D", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36,gzip(gfe)", + "clientName": "WEB_REMIX", + "clientVersion": "1.20220715.04.00", + "osName": "X11", + "osVersion": "", + "originalUrl": "https://music.youtube.com/?cbrd=1", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "configInfo": { + "appInstallData": "CMi19JYGENSDrgUQxPb9EhCurK4FEP24_RIQlK-uBRC3y60FENCtrgUQ5vj9EhDLoq4FELiLrgUQpvP9EhDYvq0F" + }, + "browserName": "Chrome", + "browserVersion": "103.0.5060.134", + "screenWidthPoints": 759, + "screenHeightPoints": 1010, + "screenPixelDensity": 1, + "screenDensityFloat": 1, + "utcOffsetMinutes": 120, + "userInterfaceTheme": "USER_INTERFACE_THEME_DARK", + "timeZone": "Europe/Berlin", + "musicAppInfo": { + "pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN", + "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", + "storeDigitalGoodsApiSupportStatus": { + "playStoreDigitalGoodsApiSupportStatus": "DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED" + }, + "musicActivityMasterSwitch": "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", + "musicLocationMasterSwitch": "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE" + } + }, + "user": { "lockedSafetyMode": false }, + "request": { + "useSsl": true, + "internalExperimentFlags": [], + "consistencyTokenJars": [] + }, + "adSignalsInfo": { + "params": [ + { "key": "dt", "value": "1658657481372" }, + { "key": "flash", "value": "0" }, + { "key": "frm", "value": "0" }, + { "key": "u_tz", "value": "120" }, + { "key": "u_his", "value": "4" }, + { "key": "u_h", "value": "1080" }, + { "key": "u_w", "value": "1920" }, + { "key": "u_ah", "value": "1080" }, + { "key": "u_aw", "value": "1920" }, + { "key": "u_cd", "value": "24" }, + { "key": "bc", "value": "31" }, + { "key": "bih", "value": "1010" }, + { "key": "biw", "value": "759" }, + { "key": "brdim", "value": "2560,0,2560,0,1920,0,1920,1080,759,1010" }, + { "key": "vis", "value": "1" }, + { "key": "wgl", "value": "true" }, + { "key": "ca_type", "value": "image" } + ] + }, + "activePlayers": [{ "playerContextParams": "Q0FFU0FnZ0I=" }] + }, + "query": "test", + "suggestStats": { + "validationStatus": "VALID", + "parameterValidationStatus": "VALID_PARAMETERS", + "clientName": "youtube-music", + "searchMethod": "ENTER_KEY", + "inputMethod": "KEYBOARD", + "originalQuery": "test", + "availableSuggestions": [ + { "index": 0, "suggestionType": 0 }, + { "index": 1, "suggestionType": 0 }, + { "index": 2, "suggestionType": 0 }, + { "index": 3, "suggestionType": 0 }, + { "index": 4, "suggestionType": 0 }, + { "index": 5, "suggestionType": 0 }, + { "index": 6, "suggestionType": 0 } + ], + "zeroPrefixEnabled": true, + "firstEditTimeMsec": 240405, + "lastEditTimeMsec": 243276 + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..609e209 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,303 @@ +use std::time::Instant; + +use anyhow::{anyhow, bail, Context, Result}; +use fancy_regex::Regex; +use log::{debug, error, info, warn}; +use once_cell::sync::Lazy; +use rand::Rng; +use reqwest::{header, Client, ClientBuilder, Request}; +use serde::Serialize; +use tokio::sync::Mutex; + +use crate::util; + +pub enum ClientType { + Desktop, + DesktopMusic, + TvHtml5Embed, + Android, + Ios, +} + +#[derive(Serialize, Debug)] +#[serde(rename = "camelCase")] +struct BaseRequest { + context: ContextYT, +} + +#[derive(Serialize, Debug)] +#[serde(rename = "camelCase")] +struct ContextYT { + client: ClientInfo, + /// only used on desktop + #[serde(skip_serializing_if = "Option::is_none")] + request: Option, + user: User, + /// only used for the embedded player + #[serde(skip_serializing_if = "Option::is_none")] + third_party: Option, +} + +#[derive(Serialize, Debug)] +#[serde(rename = "camelCase")] +struct ClientInfo { + client_name: String, + client_version: String, + #[serde(skip_serializing_if = "Option::is_none")] + client_screen: Option, + platform: String, + #[serde(skip_serializing_if = "Option::is_none")] + original_url: Option, + /// Language (`en`, `de`) + hl: String, + /// Country (`US`, `DE`) + gl: String, +} + +#[derive(Serialize, Debug)] +#[serde(rename = "camelCase")] +struct RequestYT { + internal_experiment_flags: Vec, + use_ssl: bool, +} + +impl Default for RequestYT { + fn default() -> Self { + Self { + internal_experiment_flags: vec![], + use_ssl: true, + } + } +} + +#[derive(Serialize, Debug, Default)] +#[serde(rename = "camelCase")] +struct User { + // TO DO: provide a way to enable restricted mode with: + // "enableSafetyMode": true + locked_safety_mode: bool, +} + +#[derive(Serialize, Debug)] +#[serde(rename = "camelCase")] +struct ThirdParty { + embed_url: String, +} + +struct DesktopClientData { + last_update: Option, + client_version: String, +} + +impl Default for DesktopClientData { + fn default() -> Self { + Self { + last_update: None, + client_version: DESKTOP_CLIENT_VERSION.to_owned(), + } + } +} + +impl DesktopClientData { + fn is_old(&self) -> bool { + self.last_update.is_none() + || Instant::now() + .duration_since(self.last_update.unwrap()) + .as_secs() + > 86400 + } +} + +const DEFAULT_UA: &str = + "Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0"; + +const CONSENT_COOKIE: &str = "CONSENT"; +const CONSENT_COOKIE_YES: &str = "YES+yt.462272069.de+FX+"; +const CONSENT_COOKIE_NO: &str = "PENDING+"; + +const DESKTOP_CLIENT_VERSION: &str = "2.20220721.05.00_1"; +const DESKTOP_API_KEY: &str = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; + +const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +pub struct RustyTube { + http: Client, + desktop_client_data: Mutex, + + lang: String, + country: String, + + consent_cookie_yes: String, + consent_cookie_no: String, +} + +impl RustyTube { + #[must_use] + pub fn new() -> Self { + Self::new_with_ua("en", "US", DEFAULT_UA) + } + + #[must_use] + pub fn new_with_ua(lang: &str, country: &str, user_agent: &str) -> Self { + let http = ClientBuilder::new() + .user_agent(user_agent) + .build() + .expect("unable to build the HTTP client"); + + let mut rng = rand::thread_rng(); + + Self { + http, + desktop_client_data: Mutex::new(DesktopClientData::default()), + lang: lang.to_owned(), + country: country.to_owned(), + consent_cookie_yes: format!( + "{}={}{}", + CONSENT_COOKIE, + CONSENT_COOKIE_YES, + rng.gen_range(100..1000) + ), + consent_cookie_no: format!( + "{}={}{}", + CONSENT_COOKIE, + CONSENT_COOKIE_NO, + rng.gen_range(100..1000) + ), + } + } + + async fn get_client_version(&self) -> String { + let mut client_data = self.desktop_client_data.lock().await; + + if client_data.is_old() { + let client_version = self.extract_client_version_from_swjs().await; + let new_version = match client_version { + Ok(client_version) => match client_version { + Some(client_version) => { + debug!("Updated desktop client version to {}", client_version); + client_version + } + None => { + warn!("Could not find desktop client version in sw.js"); + DESKTOP_CLIENT_VERSION.to_owned() + } + }, + Err(e) => { + warn!("Could not extract desktop client version, Error: {}", e); + DESKTOP_CLIENT_VERSION.to_owned() + } + }; + + *client_data = DesktopClientData { + client_version: new_version, + last_update: Some(Instant::now()), + } + } + + client_data.client_version.to_string() + } + + async fn extract_client_version_from_swjs(&self) -> Result> { + let swjs = self + .exec_request( + self.http + .get("https://www.youtube.com/sw.js") + .header(header::ORIGIN, "https://www.youtube.com") + .header(header::REFERER, "https://www.youtube.com") + .header(header::COOKIE, self.consent_cookie_yes.to_owned()) + .build() + .unwrap(), + ) + .await + .context("Failed to download sw.js")?; + + static CLIENT_VERSION_PATTERNS: Lazy<[Regex; 3]> = Lazy::new(|| { + [ + Regex::new("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"").unwrap(), + Regex::new("innertube_context_client_version\":\"([0-9\\.]+?)\"").unwrap(), + Regex::new("client.version=([0-9\\.]+)").unwrap(), + ] + }); + + /* + static API_KEY_PATTERNS: Lazy<[Regex; 2]> = Lazy::new(|| { + [ + Regex::new("INNERTUBE_API_KEY\":\"([0-9a-zA-Z_-]+?)\"").unwrap(), + Regex::new("innertubeApiKey\":\"([0-9a-zA-Z_-]+?)\"").unwrap(), + ] + });*/ + + Ok(util::get_cg_from_regexes( + CLIENT_VERSION_PATTERNS.iter(), + &swjs, + 1, + )) + } + + fn generate_content_playback_nonce() -> String { + util::random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 16) + } + + fn generate_t_parameter() -> String { + util::random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 12) + } + + async fn exec_request(&self, request: Request) -> Result { + let resp = self.http.execute(request).await?.error_for_status()?; + Ok(resp.text().await?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_log::test; + + #[test(tokio::test)] + async fn t_extract_client_version_from_swjs() { + let rt = RustyTube::new(); + let version = rt.extract_client_version_from_swjs().await.unwrap(); + + let version = version.unwrap(); + + // Client version changes often, notify during test so the hardcoded version can be updated + if version != DESKTOP_CLIENT_VERSION { + println!( + "INFO: YT Desktop Client was updated, new version: {}", + version + ); + } + } + + #[test(tokio::test)] + async fn t_get_client_version() { + error!("Checking whether it still works..."); + let rt = RustyTube::new(); + let client_version = rt.get_client_version().await; + assert!(client_version.len() > 10); + } + + #[test] + fn json_test() { + let request = BaseRequest { + context: ContextYT { + client: ClientInfo { + client_name: "WEB".to_owned(), + client_version: "x".to_owned(), + client_screen: None, + platform: "DESKTOP".to_owned(), + original_url: Some("https://www.youtube.com".to_owned()), + hl: "de".to_owned(), + gl: "DE".to_owned(), + }, + request: Some(RequestYT::default()), + user: User::default(), + third_party: None, + }, + }; + + let request_str = serde_json::to_string_pretty(&request).unwrap(); + println!("{}", request_str); + } +} diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index fcb9e22..8e8d1ed 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -1,13 +1,16 @@ use anyhow::{anyhow, bail, Context, Result}; use fancy_regex::Regex; +use log::debug; use once_cell::sync::Lazy; use reqwest::Client; use std::result::Result::Ok; use std::time::Instant; use tokio::sync::RwLock; +use crate::util; + pub struct Deobfuscator { - client: Client, + http: Client, cache: RwLock, } @@ -19,9 +22,10 @@ struct JSCache { } impl Deobfuscator { - pub fn new(client: Client) -> Self { + #[must_use] + pub fn new(http: Client) -> Self { Self { - client: client, + http, cache: RwLock::new(JSCache { js_url: None, last_update: Instant::now(), @@ -35,15 +39,15 @@ impl Deobfuscator { let mut cache = self.cache.write().await; if cache.is_stale() { - let url = get_player_js_url(&self.client) + let url = get_player_js_url(&self.http) .await .context("Failed to retrieve player.js URL")?; - let player_js = get_website(&self.client, &url) + let player_js = get_response(&self.http, &url) .await .context("Failed to download player.js")?; - println!("Downloaded player.js from {}", url); + debug!("Downloaded player.js from {}", url); let sig_fn = get_sig_fn(&player_js)?; let nsig_fn = get_nsig_fn(&player_js)?; @@ -91,10 +95,7 @@ fn get_sig_fn_name(player_js: &str) -> Result { ] }); - FUNCTION_PATTERNS - .iter() - .find_map(|pattern| pattern.captures(player_js).ok().flatten()) - .map(|c| c.get(1).unwrap().as_str().to_owned()) + util::get_cg_from_regexes(FUNCTION_PATTERNS.iter(), player_js, 1) .ok_or_else(|| anyhow!("could not find deobf function name")) } @@ -270,8 +271,8 @@ fn deobfuscate_nsig(sig: &str, nsig_fn: &str) -> Result { } } -async fn get_player_js_url(client: &Client) -> Result { - let resp = client +async fn get_player_js_url(http: &Client) -> Result { + let resp = http .get("https://www.youtube.com/iframe_api") .send() .await? @@ -295,8 +296,8 @@ async fn get_player_js_url(client: &Client) -> Result { )) } -async fn get_website(client: &Client, url: &str) -> Result { - let resp = client.get(url).send().await?.error_for_status()?; +async fn get_response(http: &Client, url: &str) -> Result { + let resp = http.get(url).send().await?.error_for_status()?; Ok(resp.text().await?) } @@ -304,6 +305,7 @@ async fn get_website(client: &Client, url: &str) -> Result { mod tests { use super::*; use std::sync::Arc; + use test_log::test; const TEST_JS: &str = include_str!("../notes/base.js"); const N_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))}, @@ -321,13 +323,13 @@ c[30]=c;c[40]=c;c[46]=c;try{c[43](c[34]),c[45](c[40],c[47]),c[46](c[51],c[33]),c c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c[47],c[49]),c[1](c[44],c[28]),c[39](c[16]),c[32](c[42],c[22]),c[46](c[14],c[48]),c[26](c[29],c[10]),c[46](c[9],c[3]),c[32](c[45])}catch(d){return"enhanced_except_85UBjOr-_w8_"+a}return b.join("")};function deobfuscate(a){return Vo(a);}"#; #[test] - fn test_get_sig_fn_name() { + fn t_get_sig_fn_name() { let dfunc_name = get_sig_fn_name(TEST_JS).unwrap(); assert_eq!(dfunc_name, "Rva"); } #[test] - fn test_get_sig_fn() { + fn t_get_sig_fn() { let dcode = get_sig_fn(TEST_JS).unwrap(); assert_eq!( dcode, @@ -336,47 +338,47 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c } #[test] - fn test_deobfuscate_sig() { + fn t_deobfuscate_sig() { let dcode = get_sig_fn(TEST_JS).unwrap(); let deobf = deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i", &dcode).unwrap(); assert_eq!(deobf, "AOq0QJ8wRAIgaryQHmplJ9xJSKFywyaSMHuuwZYsoMTfvRviG51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5f"); } #[test] - fn test_get_nsig_fn_name() { + fn t_get_nsig_fn_name() { let name = get_nsig_fn_name(TEST_JS).unwrap(); assert_eq!(name, "Vo"); } #[test] - fn test_match_to_closing_parenthesis() { + fn t_match_to_closing_parenthesis() { let res = match_to_closing_parenthesis("Kx Hello { Thx { Bye } } Wut {Tst {}}", "Hello").unwrap(); assert_eq!(res, " { Thx { Bye } }") } #[test] - fn test_get_nsig_fn() { + fn t_get_nsig_fn() { let res = get_nsig_fn(TEST_JS).unwrap(); assert_eq!(res, N_DEOBF_FUNC); } #[test] - fn test_deobfuscate_nsig() { + fn t_deobfuscate_nsig() { let res = deobfuscate_nsig("BI_n4PxQ22is-KKajKUW", N_DEOBF_FUNC).unwrap(); assert_eq!(res, "nrkec0fwgTWolw"); } - #[tokio::test] - async fn test_get_player_js_url() { + #[test(tokio::test)] + async fn t_get_player_js_url() { let client = Client::new(); let url = get_player_js_url(&client).await.unwrap(); assert!(url.starts_with("https://www.youtube.com/s/player")); assert_eq!(url.len(), 73); } - #[tokio::test] - async fn test_update() { + #[test(tokio::test)] + async fn t_update() { let client = Client::new(); let deobf = Deobfuscator::new(client); @@ -384,8 +386,8 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c println!("{}", deobf_sig); } - #[tokio::test] - async fn test_parallel() { + #[test(tokio::test)] + async fn t_parallel() { let client = Client::new(); let deobf = Deobfuscator::new(client); let deobf_arc = Arc::new(deobf); diff --git a/src/lib.rs b/src/lib.rs index b9f2de7..10d6f54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,9 @@ -#[macro_use] mod macros; +#[allow(dead_code)] +#[macro_use] +mod macros; + +mod util; mod deobfuscate; + +pub mod client; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..f2b119c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,28 @@ +use fancy_regex::Regex; +use rand::Rng; + +/// Return the given capture group that matches first in a list of regexes +pub fn get_cg_from_regexes<'a, I>(mut regexes: I, text: &str, cg: usize) -> Option +where + I: Iterator, +{ + regexes + .find_map(|pattern| pattern.captures(text).ok().flatten()) + .map(|c| c.get(cg).unwrap().as_str().to_owned()) +} + +/// Generates a random string with given length and byte charset. +pub fn random_string(charset: &[u8], length: usize) -> String { + let mut result = String::with_capacity(length); + let mut rng = rand::thread_rng(); + + unsafe { + for _ in 0..length { + result.push( + char::from(*charset.get_unchecked(rng.gen_range(0..charset.len()))) + ); + } + } + + result +}