From 383d2789cecca03b8c549128e609c90a7f08eeec Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 5 Jun 2023 20:31:25 -0400 Subject: [PATCH 01/12] Initial PoC of spoofing Android OAuth --- Cargo.lock | 22 +++++++++++++++++++++ Cargo.toml | 2 ++ src/client.rs | 14 +++++++++----- src/main.rs | 8 ++++++++ src/oauth.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/oauth.rs diff --git a/Cargo.lock b/Cargo.lock index 42fb6ac..e01cd2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "globset" version = "0.4.10" @@ -712,6 +723,7 @@ name = "libreddit" version = "0.30.1" dependencies = [ "askama", + "base64", "brotli", "build_html", "cached", @@ -735,6 +747,7 @@ dependencies = [ "tokio", "toml", "url", + "uuid", ] [[package]] @@ -1590,6 +1603,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 529f3c0..378fb88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ toml = "0.7.4" once_cell = "1.17.0" serde_yaml = "0.9.16" build_html = "2.2.0" +uuid = { version = "1.3.3", features = ["v4"] } +base64 = "0.21.2" [dev-dependencies] lipsum = "0.9.0" diff --git a/src/client.rs b/src/client.rs index 4c174cd..de00ea3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,18 +7,22 @@ use libflate::gzip; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use serde_json::Value; +use std::sync::RwLock; use std::{io, result::Result}; use crate::dbg_msg; +use crate::oauth::{Oauth, USER_AGENT}; use crate::server::RequestExt; -const REDDIT_URL_BASE: &str = "https://www.reddit.com"; +const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; -static CLIENT: Lazy>> = Lazy::new(|| { +pub(crate) static CLIENT: Lazy>> = Lazy::new(|| { let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build(); client::Client::builder().build(https) }); +pub(crate) static OAUTH_CLIENT: Lazy> = Lazy::new(|| RwLock::new(Oauth::new())); + /// Gets the canonical path for a resource on Reddit. This is accomplished by /// making a `HEAD` request to Reddit at the path given in `path`. /// @@ -136,9 +140,9 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo let builder = Request::builder() .method(method) .uri(&url) - .header("User-Agent", format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"))) - .header("Host", "www.reddit.com") - .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") + .header("User-Agent", USER_AGENT) + .header("Host", "oauth.reddit.com") + .header("Authorization", &format!("Bearer {}", OAUTH_CLIENT.read().unwrap().token)) .header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" }) .header("Accept-Language", "en-US,en;q=0.5") .header("Connection", "keep-alive") diff --git a/src/main.rs b/src/main.rs index d1ebf85..79aa267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod config; mod duplicates; mod instance_info; +mod oauth; mod post; mod search; mod settings; @@ -25,6 +26,8 @@ use once_cell::sync::Lazy; use server::RequestExt; use utils::{error, redirect, ThemeAssets}; +use crate::client::OAUTH_CLIENT; + mod server; // Create Services @@ -167,6 +170,11 @@ async fn main() { Lazy::force(&config::CONFIG); Lazy::force(&instance_info::INSTANCE_INFO); + // Force login of Oauth client + #[allow(clippy::await_holding_lock)] + // We don't care if we are awaiting a lock here - it's just locked once at init. + OAUTH_CLIENT.write().unwrap().login().await; + // Define default headers (added to all responses) app.default_headers = headers! { "Referrer-Policy" => "no-referrer", diff --git a/src/oauth.rs b/src/oauth.rs new file mode 100644 index 0000000..bdc44cd --- /dev/null +++ b/src/oauth.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; + +use crate::client::CLIENT; +use base64::{engine::general_purpose, Engine as _}; +use hyper::{client, Body, Method, Request}; +use serde_json::json; + +static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; + +static AUTH_ENDPOINT: &str = "https://accounts.reddit.com"; +pub(crate) static USER_AGENT: &str = "Reddit/Version 2023.21.0/Build 956283/Android 13"; + +pub(crate) struct Oauth { + // Currently unused, may be necessary if we decide to support GQL in the future + pub(crate) headers_map: HashMap, + pub(crate) token: String, +} + +impl Oauth { + pub fn new() -> Self { + let uuid = uuid::Uuid::new_v4().to_string(); + Oauth { + headers_map: HashMap::from([ + ("Client-Vendor-Id".into(), uuid.clone()), + ("X-Reddit-Device-Id".into(), uuid), + ("User-Agent".into(), USER_AGENT.to_string()), + ]), + token: String::new(), + } + } + pub async fn login(&mut self) -> Option<()> { + let url = format!("{}/api/access_token", AUTH_ENDPOINT); + let mut builder = Request::builder().method(Method::POST).uri(&url); + for (key, value) in self.headers_map.iter() { + builder = builder.header(key, value); + } + + let auth = general_purpose::STANDARD.encode(format!("{REDDIT_ANDROID_OAUTH_CLIENT_ID}:")); + builder = builder.header("Authorization", format!("Basic {auth}")); + let json = json!({ + "scopes": ["*","email","pii"] + }); + let body = Body::from(json.to_string()); + let request = builder.body(body).unwrap(); + let client: client::Client<_, hyper::Body> = CLIENT.clone(); + let resp = client.request(request).await.ok()?; + let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; + let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; + self.token = json.get("access_token")?.as_str()?.to_string(); + self.headers_map.insert("Authorization".to_owned(), format!("Bearer {}", self.token)); + Some(()) + } +} From 00355de7270e69c3729826af714a58d52de7c995 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 5 Jun 2023 20:39:56 -0400 Subject: [PATCH 02/12] Set proper headers --- src/client.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index de00ea3..df4f635 100644 --- a/src/client.rs +++ b/src/client.rs @@ -135,14 +135,24 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo // Construct the hyper client from the HTTPS connector. let client: client::Client<_, hyper::Body> = CLIENT.clone(); + let (token, vendor_id, device_id) = { + let client = OAUTH_CLIENT.read().unwrap(); + ( + client.token.clone(), + client.headers_map.get("Client-Vendor-Id").unwrap().clone(), + client.headers_map.get("X-Reddit-Device-Id").unwrap().clone(), + ) + }; // Build request to Reddit. When making a GET, request gzip compression. // (Reddit doesn't do brotli yet.) let builder = Request::builder() .method(method) .uri(&url) .header("User-Agent", USER_AGENT) + .header("Client-Vendor-Id", vendor_id) + .header("X-Reddit-Device-Id", device_id) .header("Host", "oauth.reddit.com") - .header("Authorization", &format!("Bearer {}", OAUTH_CLIENT.read().unwrap().token)) + .header("Authorization", &format!("Bearer {}", token)) .header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" }) .header("Accept-Language", "en-US,en;q=0.5") .header("Connection", "keep-alive") From 8a23616920ee962c5f34e8d3c44f20f7f100f0fa Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 5 Jun 2023 20:57:34 -0400 Subject: [PATCH 03/12] Stray space --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 79aa267..671117d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,7 +171,7 @@ async fn main() { Lazy::force(&instance_info::INSTANCE_INFO); // Force login of Oauth client - #[allow(clippy::await_holding_lock)] + #[allow(clippy::await_holding_lock)] // We don't care if we are awaiting a lock here - it's just locked once at init. OAUTH_CLIENT.write().unwrap().login().await; From e94a9c81e209def5363028c1aeff1d51c2c2cc5f Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 14:33:01 -0400 Subject: [PATCH 04/12] Add deps - rand, logging --- Cargo.lock | 125 +++++++++++++++++++++++++++++++++++++---------------- Cargo.toml | 4 ++ 2 files changed, 92 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e01cd2c..95bfa47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -222,9 +222,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" dependencies = [ "clap_builder", ] @@ -345,6 +345,25 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.1" @@ -383,9 +402,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -484,9 +503,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -586,6 +605,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.26" @@ -633,9 +658,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -671,6 +696,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" @@ -694,9 +731,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libflate" @@ -729,13 +766,17 @@ dependencies = [ "cached", "clap", "cookie", + "dotenvy", + "fastrand", "futures-lite", "hyper", "hyper-rustls", "libflate", "lipsum", + "log", "once_cell", "percent-encoding", + "pretty_env_logger", "regex", "route-recognizer", "rust-embed", @@ -768,9 +809,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -852,9 +893,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl-probe" @@ -880,22 +921,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" @@ -915,6 +956,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + [[package]] name = "proc-macro2" version = "1.0.59" @@ -964,15 +1015,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -984,11 +1026,11 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick 1.0.1", + "aho-corasick 1.0.2", "memchr", "regex-syntax", ] @@ -1355,11 +1397,20 @@ checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys 0.45.0", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -1594,9 +1645,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 378fb88..b551fd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,10 @@ serde_yaml = "0.9.16" build_html = "2.2.0" uuid = { version = "1.3.3", features = ["v4"] } base64 = "0.21.2" +fastrand = "1.9.0" +log = "0.4.18" +pretty_env_logger = "0.5.0" +dotenvy = "0.15.7" [dev-dependencies] lipsum = "0.9.0" From a5833dc05cb329e04823248625b3722b452673ce Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 15:04:06 -0400 Subject: [PATCH 05/12] Add .env to .gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3076ba8..323a69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target - +.env # Idea Files -.idea/ \ No newline at end of file +.idea/ From 659a82bf63434e3ad0864cd02433a47990cfa6dc Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 15:05:20 -0400 Subject: [PATCH 06/12] Improve spoofing of devices, handle token refreshes --- src/client.rs | 16 +++-- src/main.rs | 14 +++-- src/oauth.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 174 insertions(+), 27 deletions(-) diff --git a/src/client.rs b/src/client.rs index df4f635..fad981e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,11 +7,12 @@ use libflate::gzip; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use serde_json::Value; -use std::sync::RwLock; +use std::sync::Arc; use std::{io, result::Result}; +use tokio::sync::RwLock; use crate::dbg_msg; -use crate::oauth::{Oauth, USER_AGENT}; +use crate::oauth::Oauth; use crate::server::RequestExt; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; @@ -21,7 +22,7 @@ pub(crate) static CLIENT: Lazy>> = Lazy::ne client::Client::builder().build(https) }); -pub(crate) static OAUTH_CLIENT: Lazy> = Lazy::new(|| RwLock::new(Oauth::new())); +pub(crate) static OAUTH_CLIENT: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(Oauth::new()))); /// Gets the canonical path for a resource on Reddit. This is accomplished by /// making a `HEAD` request to Reddit at the path given in `path`. @@ -135,12 +136,14 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo // Construct the hyper client from the HTTPS connector. let client: client::Client<_, hyper::Body> = CLIENT.clone(); - let (token, vendor_id, device_id) = { - let client = OAUTH_CLIENT.read().unwrap(); + let (token, vendor_id, device_id, user_agent, loid) = { + let client = OAUTH_CLIENT.blocking_read(); ( client.token.clone(), client.headers_map.get("Client-Vendor-Id").unwrap().clone(), client.headers_map.get("X-Reddit-Device-Id").unwrap().clone(), + client.headers_map.get("User-Agent").unwrap().clone(), + client.headers_map.get("x-reddit-loid").unwrap().clone(), ) }; // Build request to Reddit. When making a GET, request gzip compression. @@ -148,9 +151,10 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo let builder = Request::builder() .method(method) .uri(&url) - .header("User-Agent", USER_AGENT) + .header("User-Agent", user_agent) .header("Client-Vendor-Id", vendor_id) .header("X-Reddit-Device-Id", device_id) + .header("x-reddit-loid", loid) .header("Host", "oauth.reddit.com") .header("Authorization", &format!("Bearer {}", token)) .header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" }) diff --git a/src/main.rs b/src/main.rs index 671117d..6662e45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,6 @@ use once_cell::sync::Lazy; use server::RequestExt; use utils::{error, redirect, ThemeAssets}; -use crate::client::OAUTH_CLIENT; - mod server; // Create Services @@ -111,6 +109,12 @@ async fn style() -> Result, String> { #[tokio::main] async fn main() { + // Load environment variables + dotenvy::dotenv().unwrap(); + + // Initialize logger + pretty_env_logger::init(); + let matches = Command::new("Libreddit") .version(env!("CARGO_PKG_VERSION")) .about("Private front-end for Reddit written in Rust ") @@ -170,10 +174,8 @@ async fn main() { Lazy::force(&config::CONFIG); Lazy::force(&instance_info::INSTANCE_INFO); - // Force login of Oauth client - #[allow(clippy::await_holding_lock)] - // We don't care if we are awaiting a lock here - it's just locked once at init. - OAUTH_CLIENT.write().unwrap().login().await; + // Initialize OAuth client spoofing + oauth::initialize().await; // Define default headers (added to all responses) app.default_headers = headers! { diff --git a/src/oauth.rs b/src/oauth.rs index bdc44cd..ef5be7e 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -1,53 +1,194 @@ -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; -use crate::client::CLIENT; +use crate::client::{CLIENT, OAUTH_CLIENT}; use base64::{engine::general_purpose, Engine as _}; use hyper::{client, Body, Method, Request}; +use log::info; use serde_json::json; static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; +static REDDIT_IOS_OAUTH_CLIENT_ID: &str = "LNDo9k1o8UAEUw"; static AUTH_ENDPOINT: &str = "https://accounts.reddit.com"; -pub(crate) static USER_AGENT: &str = "Reddit/Version 2023.21.0/Build 956283/Android 13"; + +// Various Android user agents - build numbers from valid APK variants +pub(crate) static ANDROID_USER_AGENT: [&str; 3] = [ + "Reddit/Version 2023.21.0/Build 956283/Android 13", + "Reddit/Version 2023.21.0/Build 968223/Android 10", + "Reddit/Version 2023.21.0/Build 946732/Android 12", +]; + +// Various iOS user agents - iOS versions. +pub(crate) static IOS_USER_AGENT: [&str; 3] = [ + "Reddit/Version 2023.22.0/Build 613580/iOS Version 17.0 (Build 21A5248V)", + "Reddit/Version 2023.22.0/Build 613580/iOS Version 16.0 (Build 20A5328h)", + "Reddit/Version 2023.22.0/Build 613580/iOS Version 16.5", +]; +// Various iOS device codes. iPhone 11 displays as `iPhone12,1` +// This is a bit of a hack, but I just changed the number a few times +pub(crate) static IOS_DEVICES: [&str; 5] = ["iPhone8,1", "iPhone11,1", "iPhone12,1", "iPhone13,1", "iPhone14,1"]; pub(crate) struct Oauth { // Currently unused, may be necessary if we decide to support GQL in the future pub(crate) headers_map: HashMap, pub(crate) token: String, + expires_in: u64, + device: Device, } impl Oauth { - pub fn new() -> Self { - let uuid = uuid::Uuid::new_v4().to_string(); + pub(crate) fn new() -> Self { + // Generate a random device to spoof + let device = Device::random(); + let headers = device.headers.clone(); + // For now, just insert headers - no token request Oauth { - headers_map: HashMap::from([ - ("Client-Vendor-Id".into(), uuid.clone()), - ("X-Reddit-Device-Id".into(), uuid), - ("User-Agent".into(), USER_AGENT.to_string()), - ]), + headers_map: headers, token: String::new(), + expires_in: 0, + device, } } - pub async fn login(&mut self) -> Option<()> { + async fn login(&mut self) -> Option<()> { + // Construct URL for OAuth token let url = format!("{}/api/access_token", AUTH_ENDPOINT); let mut builder = Request::builder().method(Method::POST).uri(&url); - for (key, value) in self.headers_map.iter() { - builder = builder.header(key, value); - } - let auth = general_purpose::STANDARD.encode(format!("{REDDIT_ANDROID_OAUTH_CLIENT_ID}:")); + // Add headers from spoofed client + for (key, value) in self.headers_map.iter() { + // Skip Authorization header - won't be present in `Device` struct + // and will only be there in subsequent token refreshes. + // Sending a bearer auth token when requesting one is a bad idea + // Normally, you'd want to send it along to authenticate a refreshed token, + // but neither Android nor iOS does this - it just requests a new token. + // We try to match behavior as closely as possible. + if key != "Authorization" { + builder = builder.header(key, value); + } + } + // Set up HTTP Basic Auth - basically just the const OAuth ID's with no password, + // Base64-encoded. https://en.wikipedia.org/wiki/Basic_access_authentication + // This could be constant, but I don't think it's worth it. OAuth ID's can change + // over time and we want to be flexible. + let auth = general_purpose::STANDARD.encode(format!("{}:", self.device.oauth_id)); builder = builder.header("Authorization", format!("Basic {auth}")); + + // Set JSON body. I couldn't tell you what this means. But that's what the client sends let json = json!({ "scopes": ["*","email","pii"] }); let body = Body::from(json.to_string()); + + // Build request let request = builder.body(body).unwrap(); + info!("Request: {request:?}"); + + // Send request let client: client::Client<_, hyper::Body> = CLIENT.clone(); let resp = client.request(request).await.ok()?; + + // Parse headers - loid header _should_ be saved sent on subsequent token refreshes. + // Technically it's not needed, but it's easy for Reddit API to check for this. + // It's some kind of header that uniquely identifies the device. + if let Some(header) = resp.headers().get("x-reddit-loid") { + self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string()); + } + + info!("OAuth response: {resp:?}"); + // Serialize response let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; + + // Save token and expiry self.token = json.get("access_token")?.as_str()?.to_string(); + self.expires_in = json.get("expires_in")?.as_u64()?; self.headers_map.insert("Authorization".to_owned(), format!("Bearer {}", self.token)); + + info!("Retrieved token {}, expires in {}", self.token, self.expires_in); + Some(()) } + + async fn refresh(&mut self) -> Option<()> { + // Refresh is actually just a subsequent login with the same headers (without the old token + // or anything). This logic is handled in login, so we just call login again. + let refresh = self.login().await; + info!("Refreshing OAuth token... {}", if refresh.is_some() { "success" } else { "failed" }); + refresh + } +} +// Initialize the OAuth client and launch a thread to monitor subsequent token refreshes. +pub(crate) async fn initialize() { + // Initial login + OAUTH_CLIENT.write().await.login().await.unwrap(); + // Spawn token daemon in background - we want the initial login to be blocked upon, but the + // daemon to be launched in the background. + // Initial login blocks libreddit start-up because we _need_ the oauth token. + tokio::spawn(token_daemon()); +} +async fn token_daemon() { + // Monitor for refreshing token + loop { + // Get expiry time - be sure to not hold the read lock + let expires_in = OAUTH_CLIENT.read().await.expires_in; + + // sleep for the expiry time minus 2 minutes + let duration = Duration::from_secs(expires_in - 120); + tokio::time::sleep(duration).await; + + info!("[{duration:?} ELAPSED] Refreshing OAuth token..."); + + // Refresh token - in its own scope + { + let mut client = OAUTH_CLIENT.write().await; + client.refresh().await; + } + } +} +#[derive(Debug)] +struct Device { + oauth_id: String, + headers: HashMap, +} + +impl Device { + fn android() -> Self { + let uuid = uuid::Uuid::new_v4().to_string(); + let android_user_agent = ANDROID_USER_AGENT[fastrand::usize(..ANDROID_USER_AGENT.len())].to_string(); + let headers = HashMap::from([ + ("Client-Vendor-Id".into(), uuid.clone()), + ("X-Reddit-Device-Id".into(), uuid.clone()), + ("User-Agent".into(), android_user_agent), + ]); + info!("Spoofing Android client with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_ANDROID_OAUTH_CLIENT_ID}\""); + Device { + oauth_id: REDDIT_ANDROID_OAUTH_CLIENT_ID.to_string(), + headers, + } + } + fn ios() -> Self { + let uuid = uuid::Uuid::new_v4().to_string(); + let ios_user_agent = IOS_USER_AGENT[fastrand::usize(..IOS_USER_AGENT.len())].to_string(); + let ios_device = IOS_DEVICES[fastrand::usize(..IOS_DEVICES.len())].to_string(); + let headers = HashMap::from([ + ("X-Reddit-DPR".into(), "2".into()), + ("Device-Name".into(), ios_device.clone()), + ("X-Reddit-Device-Id".into(), uuid.clone()), + ("User-Agent".into(), ios_user_agent), + ("Client-Vendor-Id".into(), uuid.clone()), + ]); + info!("Spoofing iOS client {ios_device} with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_IOS_OAUTH_CLIENT_ID}\""); + Device { + oauth_id: REDDIT_IOS_OAUTH_CLIENT_ID.to_string(), + headers, + } + } + // Randomly choose a device + fn random() -> Self { + if fastrand::bool() { + Device::android() + } else { + Device::ios() + } + } } From dc7601375e7990ee534b9b7876def91c02bdbbcb Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 15:07:11 -0400 Subject: [PATCH 07/12] Ignore dotenv failure --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 6662e45..3a0802e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,7 +110,7 @@ async fn style() -> Result, String> { #[tokio::main] async fn main() { // Load environment variables - dotenvy::dotenv().unwrap(); + _ = dotenvy::dotenv(); // Initialize logger pretty_env_logger::init(); From 6cd53abd420147e986014387d37df0da19ea02da Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 15:26:31 -0400 Subject: [PATCH 08/12] Documentation --- src/oauth.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/oauth.rs b/src/oauth.rs index ef5be7e..a600d88 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -153,23 +153,37 @@ struct Device { impl Device { fn android() -> Self { + // Generate uuid let uuid = uuid::Uuid::new_v4().to_string(); + + // Select random user agent from ANDROID_USER_AGENT let android_user_agent = ANDROID_USER_AGENT[fastrand::usize(..ANDROID_USER_AGENT.len())].to_string(); + + // Android device headers let headers = HashMap::from([ ("Client-Vendor-Id".into(), uuid.clone()), ("X-Reddit-Device-Id".into(), uuid.clone()), ("User-Agent".into(), android_user_agent), ]); + info!("Spoofing Android client with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_ANDROID_OAUTH_CLIENT_ID}\""); + Device { oauth_id: REDDIT_ANDROID_OAUTH_CLIENT_ID.to_string(), headers, } } fn ios() -> Self { + // Generate uuid let uuid = uuid::Uuid::new_v4().to_string(); + + // Select random user agent from IOS_USER_AGENT let ios_user_agent = IOS_USER_AGENT[fastrand::usize(..IOS_USER_AGENT.len())].to_string(); + + // Select random iOS device from IOS_DEVICES let ios_device = IOS_DEVICES[fastrand::usize(..IOS_DEVICES.len())].to_string(); + + // iOS device headers let headers = HashMap::from([ ("X-Reddit-DPR".into(), "2".into()), ("Device-Name".into(), ios_device.clone()), @@ -177,7 +191,9 @@ impl Device { ("User-Agent".into(), ios_user_agent), ("Client-Vendor-Id".into(), uuid.clone()), ]); + info!("Spoofing iOS client {ios_device} with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_IOS_OAUTH_CLIENT_ID}\""); + Device { oauth_id: REDDIT_IOS_OAUTH_CLIENT_ID.to_string(), headers, From 0ca0eefaa44fe36a8cb77acecb52c9f87bff0d88 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Tue, 6 Jun 2023 15:28:36 -0400 Subject: [PATCH 09/12] Add tests to check fetching sub/user/oauth --- src/client.rs | 4 ++-- src/oauth.rs | 11 +++++++++++ src/subreddit.rs | 5 +++++ src/user.rs | 7 +++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index fad981e..861b210 100644 --- a/src/client.rs +++ b/src/client.rs @@ -137,13 +137,13 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo let client: client::Client<_, hyper::Body> = CLIENT.clone(); let (token, vendor_id, device_id, user_agent, loid) = { - let client = OAUTH_CLIENT.blocking_read(); + let client = tokio::task::block_in_place(move || OAUTH_CLIENT.blocking_read()); ( client.token.clone(), client.headers_map.get("Client-Vendor-Id").unwrap().clone(), client.headers_map.get("X-Reddit-Device-Id").unwrap().clone(), client.headers_map.get("User-Agent").unwrap().clone(), - client.headers_map.get("x-reddit-loid").unwrap().clone(), + client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), ) }; // Build request to Reddit. When making a GET, request gzip compression. diff --git a/src/oauth.rs b/src/oauth.rs index a600d88..7851e07 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -208,3 +208,14 @@ impl Device { } } } + +#[tokio::test] +async fn test_oauth_client() { + initialize().await; +} + +#[tokio::test] +async fn test_oauth_client_refresh() { + initialize().await; + OAUTH_CLIENT.write().await.refresh().await.unwrap(); +} diff --git a/src/subreddit.rs b/src/subreddit.rs index ee57ea5..05bbd64 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -434,3 +434,8 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { nsfw: res["data"]["over18"].as_bool().unwrap_or_default(), }) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fetching_subreddit() { + subreddit("rust", false).await.unwrap(); +} diff --git a/src/user.rs b/src/user.rs index efa70a9..a5b8d97 100644 --- a/src/user.rs +++ b/src/user.rs @@ -129,3 +129,10 @@ async fn user(name: &str) -> Result { } }) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fetching_user() { + let user = user("spez").await; + assert!(user.is_ok()); + assert!(user.unwrap().karma > 100); +} From 49dde7ad7257944dace7e96f2727bfc2c7b5c3dd Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 8 Jun 2023 14:06:58 -0400 Subject: [PATCH 10/12] Improve subreddit test --- src/subreddit.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index 05bbd64..1a9b921 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -437,5 +437,6 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fetching_subreddit() { - subreddit("rust", false).await.unwrap(); + let subreddit = subreddit("rust", false).await; + assert!(subreddit.is_ok()); } From c00beaa5d8c5abfcee795a77b514295d48460277 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 8 Jun 2023 14:33:54 -0400 Subject: [PATCH 11/12] Improve OAuth refresh, logging --- src/client.rs | 19 ++++++++++++------- src/main.rs | 14 ++++++++++---- src/oauth.rs | 41 ++++++++++++++++++++--------------------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/client.rs b/src/client.rs index 861b210..a6342d1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ use cached::proc_macro::cached; +use futures_lite::future::block_on; use futures_lite::{future::Boxed, FutureExt}; use hyper::client::HttpConnector; use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri}; @@ -7,12 +8,12 @@ use libflate::gzip; use once_cell::sync::Lazy; use percent_encoding::{percent_encode, CONTROLS}; use serde_json::Value; -use std::sync::Arc; + use std::{io, result::Result}; use tokio::sync::RwLock; use crate::dbg_msg; -use crate::oauth::Oauth; +use crate::oauth::{token_daemon, Oauth}; use crate::server::RequestExt; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; @@ -22,7 +23,11 @@ pub(crate) static CLIENT: Lazy>> = Lazy::ne client::Client::builder().build(https) }); -pub(crate) static OAUTH_CLIENT: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(Oauth::new()))); +pub(crate) static OAUTH_CLIENT: Lazy> = Lazy::new(|| { + let client = block_on(Oauth::new()); + tokio::spawn(token_daemon()); + RwLock::new(client) +}); /// Gets the canonical path for a resource on Reddit. This is accomplished by /// making a `HEAD` request to Reddit at the path given in `path`. @@ -137,12 +142,12 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo let client: client::Client<_, hyper::Body> = CLIENT.clone(); let (token, vendor_id, device_id, user_agent, loid) = { - let client = tokio::task::block_in_place(move || OAUTH_CLIENT.blocking_read()); + let client = block_on(OAUTH_CLIENT.read()); ( client.token.clone(), - client.headers_map.get("Client-Vendor-Id").unwrap().clone(), - client.headers_map.get("X-Reddit-Device-Id").unwrap().clone(), - client.headers_map.get("User-Agent").unwrap().clone(), + client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(), + client.headers_map.get("X-Reddit-Device-Id").cloned().unwrap_or_default(), + client.headers_map.get("User-Agent").cloned().unwrap_or_default(), client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), ) }; diff --git a/src/main.rs b/src/main.rs index 3a0802e..c58d49e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,10 +22,13 @@ use hyper::{header::HeaderValue, Body, Request, Response}; mod client; use client::{canonical_path, proxy}; +use log::info; use once_cell::sync::Lazy; use server::RequestExt; use utils::{error, redirect, ThemeAssets}; +use crate::client::OAUTH_CLIENT; + mod server; // Create Services @@ -169,13 +172,16 @@ async fn main() { // Force evaluation of statics. In instance_info case, we need to evaluate // the timestamp so deploy date is accurate - in config case, we need to - // evaluate the configuration to avoid paying penalty at first request. + // evaluate the configuration to avoid paying penalty at first request - + // in OAUTH case, we need to retrieve the token to avoid paying penalty + // at first request + info!("Evaluating config."); Lazy::force(&config::CONFIG); + info!("Evaluating instance info."); Lazy::force(&instance_info::INSTANCE_INFO); - - // Initialize OAuth client spoofing - oauth::initialize().await; + info!("Creating OAUTH client."); + Lazy::force(&OAUTH_CLIENT); // Define default headers (added to all responses) app.default_headers = headers! { diff --git a/src/oauth.rs b/src/oauth.rs index 7851e07..9885a83 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -4,6 +4,7 @@ use crate::client::{CLIENT, OAUTH_CLIENT}; use base64::{engine::general_purpose, Engine as _}; use hyper::{client, Body, Method, Request}; use log::info; + use serde_json::json; static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; @@ -25,9 +26,10 @@ pub(crate) static IOS_USER_AGENT: [&str; 3] = [ "Reddit/Version 2023.22.0/Build 613580/iOS Version 16.5", ]; // Various iOS device codes. iPhone 11 displays as `iPhone12,1` -// This is a bit of a hack, but I just changed the number a few times +// I just changed the number a few times for some plausible values pub(crate) static IOS_DEVICES: [&str; 5] = ["iPhone8,1", "iPhone11,1", "iPhone12,1", "iPhone13,1", "iPhone14,1"]; +#[derive(Debug, Clone, Default)] pub(crate) struct Oauth { // Currently unused, may be necessary if we decide to support GQL in the future pub(crate) headers_map: HashMap, @@ -37,7 +39,12 @@ pub(crate) struct Oauth { } impl Oauth { - pub(crate) fn new() -> Self { + pub(crate) async fn new() -> Self { + let mut oauth = Oauth::default(); + oauth.login().await; + oauth + } + pub(crate) fn default() -> Self { // Generate a random device to spoof let device = Device::random(); let headers = device.headers.clone(); @@ -81,7 +88,6 @@ impl Oauth { // Build request let request = builder.body(body).unwrap(); - info!("Request: {request:?}"); // Send request let client: client::Client<_, hyper::Body> = CLIENT.clone(); @@ -94,7 +100,6 @@ impl Oauth { self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string()); } - info!("OAuth response: {resp:?}"); // Serialize response let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; @@ -104,7 +109,7 @@ impl Oauth { self.expires_in = json.get("expires_in")?.as_u64()?; self.headers_map.insert("Authorization".to_owned(), format!("Bearer {}", self.token)); - info!("Retrieved token {}, expires in {}", self.token, self.expires_in); + info!("✅ Success - Retrieved token \"{}...\", expires in {}", &self.token[..32], self.expires_in); Some(()) } @@ -117,23 +122,18 @@ impl Oauth { refresh } } -// Initialize the OAuth client and launch a thread to monitor subsequent token refreshes. -pub(crate) async fn initialize() { - // Initial login - OAUTH_CLIENT.write().await.login().await.unwrap(); - // Spawn token daemon in background - we want the initial login to be blocked upon, but the - // daemon to be launched in the background. - // Initial login blocks libreddit start-up because we _need_ the oauth token. - tokio::spawn(token_daemon()); -} -async fn token_daemon() { + +pub(crate) async fn token_daemon() { // Monitor for refreshing token loop { // Get expiry time - be sure to not hold the read lock - let expires_in = OAUTH_CLIENT.read().await.expires_in; + let expires_in = { OAUTH_CLIENT.read().await.expires_in }; // sleep for the expiry time minus 2 minutes let duration = Duration::from_secs(expires_in - 120); + + info!("Waiting for {duration:?} seconds before refreshing OAuth token..."); + tokio::time::sleep(duration).await; info!("[{duration:?} ELAPSED] Refreshing OAuth token..."); @@ -145,7 +145,7 @@ async fn token_daemon() { } } } -#[derive(Debug)] +#[derive(Debug, Clone, Default)] struct Device { oauth_id: String, headers: HashMap, @@ -209,13 +209,12 @@ impl Device { } } -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_oauth_client() { - initialize().await; + assert!(!OAUTH_CLIENT.read().await.token.is_empty()); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_oauth_client_refresh() { - initialize().await; OAUTH_CLIENT.write().await.refresh().await.unwrap(); } From f7f1aa4bde6be9f80183cbf13e023c994b7f04fb Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Thu, 8 Jun 2023 16:27:36 -0400 Subject: [PATCH 12/12] Abstract out random choosing --- src/oauth.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/oauth.rs b/src/oauth.rs index 9885a83..951ab75 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -157,7 +157,7 @@ impl Device { let uuid = uuid::Uuid::new_v4().to_string(); // Select random user agent from ANDROID_USER_AGENT - let android_user_agent = ANDROID_USER_AGENT[fastrand::usize(..ANDROID_USER_AGENT.len())].to_string(); + let android_user_agent = choose(&ANDROID_USER_AGENT).to_string(); // Android device headers let headers = HashMap::from([ @@ -178,10 +178,10 @@ impl Device { let uuid = uuid::Uuid::new_v4().to_string(); // Select random user agent from IOS_USER_AGENT - let ios_user_agent = IOS_USER_AGENT[fastrand::usize(..IOS_USER_AGENT.len())].to_string(); + let ios_user_agent = choose(&IOS_USER_AGENT).to_string(); // Select random iOS device from IOS_DEVICES - let ios_device = IOS_DEVICES[fastrand::usize(..IOS_DEVICES.len())].to_string(); + let ios_device = choose(&IOS_DEVICES).to_string(); // iOS device headers let headers = HashMap::from([ @@ -209,6 +209,12 @@ impl Device { } } +// Waiting on fastrand 2.0.0 for the `choose` function +// https://github.com/smol-rs/fastrand/pull/59/ +fn choose(list: &[T]) -> T { + list[fastrand::usize(..list.len())] +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_oauth_client() { assert!(!OAUTH_CLIENT.read().await.token.is_empty());