Merge remote-tracking branch 'origin/pull/819'
This commit is contained in:
commit
90d1831352
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
|
.env
|
||||||
# Idea Files
|
# Idea Files
|
||||||
.idea/
|
.idea/
|
||||||
|
143
Cargo.lock
generated
143
Cargo.lock
generated
@ -19,9 +19,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@ -222,9 +222,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.3.1"
|
version = "4.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28"
|
checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
]
|
]
|
||||||
@ -345,6 +345,25 @@ dependencies = [
|
|||||||
"crypto-common",
|
"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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -383,9 +402,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
@ -482,6 +501,17 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "globset"
|
name = "globset"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@ -575,6 +605,12 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.26"
|
version = "0.14.26"
|
||||||
@ -622,9 +658,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
@ -660,6 +696,18 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@ -683,9 +731,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.144"
|
version = "0.2.146"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libflate"
|
name = "libflate"
|
||||||
@ -712,18 +760,23 @@ name = "libreddit"
|
|||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
|
"base64",
|
||||||
"brotli",
|
"brotli",
|
||||||
"build_html",
|
"build_html",
|
||||||
"cached",
|
"cached",
|
||||||
"clap",
|
"clap",
|
||||||
"cookie",
|
"cookie",
|
||||||
|
"dotenvy",
|
||||||
|
"fastrand",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"libflate",
|
"libflate",
|
||||||
"lipsum",
|
"lipsum",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"pretty_env_logger",
|
||||||
"regex",
|
"regex",
|
||||||
"route-recognizer",
|
"route-recognizer",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@ -735,6 +788,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -755,9 +809,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.9"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
@ -839,9 +893,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.17.2"
|
version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
@ -867,22 +921,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot_core"
|
name = "parking_lot_core"
|
||||||
version = "0.9.7"
|
version = "0.9.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
|
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall 0.2.16",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-sys 0.45.0",
|
"windows-targets 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
@ -902,6 +956,16 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.59"
|
version = "1.0.59"
|
||||||
@ -951,15 +1015,6 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@ -971,11 +1026,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.8.3"
|
version = "1.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
|
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick 1.0.1",
|
"aho-corasick 1.0.2",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
@ -1342,11 +1397,20 @@ checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"redox_syscall 0.3.5",
|
"redox_syscall",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.45.0",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@ -1581,15 +1645,24 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -30,6 +30,12 @@ toml = "0.7.4"
|
|||||||
once_cell = "1.17.0"
|
once_cell = "1.17.0"
|
||||||
serde_yaml = "0.9.16"
|
serde_yaml = "0.9.16"
|
||||||
build_html = "2.2.0"
|
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]
|
[dev-dependencies]
|
||||||
lipsum = "0.9.0"
|
lipsum = "0.9.0"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
|
use futures_lite::future::block_on;
|
||||||
use futures_lite::{future::Boxed, FutureExt};
|
use futures_lite::{future::Boxed, FutureExt};
|
||||||
use hyper::client::HttpConnector;
|
use hyper::client::HttpConnector;
|
||||||
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
|
||||||
@ -7,19 +8,28 @@ use libflate::gzip;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use percent_encoding::{percent_encode, CONTROLS};
|
use percent_encoding::{percent_encode, CONTROLS};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{io, result::Result, sync::atomic::Ordering::SeqCst};
|
|
||||||
|
|
||||||
use crate::instance_info::INSTANCE_INFO;
|
use std::{io, result::Result};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::dbg_msg;
|
||||||
|
use crate::oauth::{token_daemon, Oauth};
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::{config, dbg_msg};
|
use crate::{config, dbg_msg};
|
||||||
|
|
||||||
const REDDIT_URL_BASE: &str = "https://www.reddit.com";
|
const REDDIT_URL_BASE: &str = "https://oauth.reddit.com";
|
||||||
|
|
||||||
static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
pub(crate) static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
|
||||||
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||||
client::Client::builder().build(https)
|
client::Client::builder().build(https)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub(crate) static OAUTH_CLIENT: Lazy<RwLock<Oauth>> = 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
|
/// 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`.
|
/// making a `HEAD` request to Reddit at the path given in `path`.
|
||||||
///
|
///
|
||||||
@ -135,14 +145,27 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
|
|||||||
// Construct the hyper client from the HTTPS connector.
|
// Construct the hyper client from the HTTPS connector.
|
||||||
let client: client::Client<_, hyper::Body> = CLIENT.clone();
|
let client: client::Client<_, hyper::Body> = CLIENT.clone();
|
||||||
|
|
||||||
|
let (token, vendor_id, device_id, user_agent, loid) = {
|
||||||
|
let client = block_on(OAUTH_CLIENT.read());
|
||||||
|
(
|
||||||
|
client.token.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(),
|
||||||
|
)
|
||||||
|
};
|
||||||
// Build request to Reddit. When making a GET, request gzip compression.
|
// Build request to Reddit. When making a GET, request gzip compression.
|
||||||
// (Reddit doesn't do brotli yet.)
|
// (Reddit doesn't do brotli yet.)
|
||||||
let builder = Request::builder()
|
let builder = Request::builder()
|
||||||
.method(method)
|
.method(method)
|
||||||
.uri(&url)
|
.uri(&url)
|
||||||
.header("User-Agent", concat!("web:libreddit:", env!("CARGO_PKG_VERSION")))
|
.header("User-Agent", user_agent)
|
||||||
.header("Host", "www.reddit.com")
|
.header("Client-Vendor-Id", vendor_id)
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
.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" })
|
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
|
||||||
.header("Accept-Language", "en-US,en;q=0.5")
|
.header("Accept-Language", "en-US,en;q=0.5")
|
||||||
.header("Connection", "keep-alive")
|
.header("Connection", "keep-alive")
|
||||||
|
18
src/main.rs
18
src/main.rs
@ -6,6 +6,7 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod duplicates;
|
mod duplicates;
|
||||||
mod instance_info;
|
mod instance_info;
|
||||||
|
mod oauth;
|
||||||
mod post;
|
mod post;
|
||||||
mod search;
|
mod search;
|
||||||
mod settings;
|
mod settings;
|
||||||
@ -21,10 +22,13 @@ use hyper::{header::HeaderValue, Body, Request, Response};
|
|||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
use client::{canonical_path, proxy};
|
use client::{canonical_path, proxy};
|
||||||
|
use log::info;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use server::RequestExt;
|
use server::RequestExt;
|
||||||
use utils::{error, redirect, ThemeAssets};
|
use utils::{error, redirect, ThemeAssets};
|
||||||
|
|
||||||
|
use crate::client::OAUTH_CLIENT;
|
||||||
|
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
// Create Services
|
// Create Services
|
||||||
@ -108,6 +112,12 @@ async fn style() -> Result<Response<Body>, String> {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
// Load environment variables
|
||||||
|
_ = dotenvy::dotenv();
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let matches = Command::new("Libreddit")
|
let matches = Command::new("Libreddit")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("Private front-end for Reddit written in Rust ")
|
.about("Private front-end for Reddit written in Rust ")
|
||||||
@ -162,10 +172,16 @@ async fn main() {
|
|||||||
|
|
||||||
// Force evaluation of statics. In instance_info case, we need to evaluate
|
// 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
|
// 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);
|
Lazy::force(&config::CONFIG);
|
||||||
|
info!("Evaluating instance info.");
|
||||||
Lazy::force(&instance_info::INSTANCE_INFO);
|
Lazy::force(&instance_info::INSTANCE_INFO);
|
||||||
|
info!("Creating OAUTH client.");
|
||||||
|
Lazy::force(&OAUTH_CLIENT);
|
||||||
|
|
||||||
// Define default headers (added to all responses)
|
// Define default headers (added to all responses)
|
||||||
app.default_headers = headers! {
|
app.default_headers = headers! {
|
||||||
|
226
src/oauth.rs
Normal file
226
src/oauth.rs
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
// 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`
|
||||||
|
// 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<String, String>,
|
||||||
|
pub(crate) token: String,
|
||||||
|
expires_in: u64,
|
||||||
|
device: Device,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Oauth {
|
||||||
|
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();
|
||||||
|
// For now, just insert headers - no token request
|
||||||
|
Oauth {
|
||||||
|
headers_map: headers,
|
||||||
|
token: String::new(),
|
||||||
|
expires_in: 0,
|
||||||
|
device,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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!("✅ Success - Retrieved token \"{}...\", expires in {}", &self.token[..32], 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
||||||
|
// 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...");
|
||||||
|
|
||||||
|
// Refresh token - in its own scope
|
||||||
|
{
|
||||||
|
let mut client = OAUTH_CLIENT.write().await;
|
||||||
|
client.refresh().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Device {
|
||||||
|
oauth_id: String,
|
||||||
|
headers: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = choose(&ANDROID_USER_AGENT).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 = choose(&IOS_USER_AGENT).to_string();
|
||||||
|
|
||||||
|
// Select random iOS device from IOS_DEVICES
|
||||||
|
let ios_device = choose(&IOS_DEVICES).to_string();
|
||||||
|
|
||||||
|
// iOS device headers
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting on fastrand 2.0.0 for the `choose` function
|
||||||
|
// https://github.com/smol-rs/fastrand/pull/59/
|
||||||
|
fn choose<T: Copy>(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_oauth_client_refresh() {
|
||||||
|
OAUTH_CLIENT.write().await.refresh().await.unwrap();
|
||||||
|
}
|
@ -442,3 +442,9 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
|||||||
nsfw: res["data"]["over18"].as_bool().unwrap_or_default(),
|
nsfw: res["data"]["over18"].as_bool().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_fetching_subreddit() {
|
||||||
|
let subreddit = subreddit("rust", false).await;
|
||||||
|
assert!(subreddit.is_ok());
|
||||||
|
}
|
||||||
|
@ -129,3 +129,10 @@ async fn user(name: &str) -> Result<User, String> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user