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(()) + } +}