libreddit/src/client.rs

170 lines
5.1 KiB
Rust
Raw Normal View History

2021-03-17 23:30:33 +01:00
use cached::proc_macro::cached;
use futures_lite::{future::Boxed, FutureExt};
use hyper::{body::Buf, client, Body, Request, Response, Uri};
use percent_encoding::{percent_encode, CONTROLS};
2021-03-17 23:30:33 +01:00
use serde_json::Value;
2021-11-30 07:29:41 +01:00
use std::result::Result;
2021-03-17 23:30:33 +01:00
use crate::server::RequestExt;
pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, String> {
let mut url = format!("{}?{}", format, req.uri().query().unwrap_or_default());
2021-03-17 23:30:33 +01:00
2021-05-20 21:24:06 +02:00
// For each parameter in request
2021-03-17 23:30:33 +01:00
for (name, value) in req.params().iter() {
2021-05-20 21:24:06 +02:00
// Fill the parameter value in the url
2021-03-17 23:30:33 +01:00
url = url.replace(&format!("{{{}}}", name), value);
}
stream(&url, &req).await
2021-03-17 23:30:33 +01:00
}
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
2021-03-17 23:30:33 +01:00
// First parameter is target URL (mandatory).
2021-11-30 07:29:41 +01:00
let uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
2021-03-17 23:30:33 +01:00
// Prepare the HTTPS connector.
2021-11-22 07:44:05 +01:00
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
2021-03-17 23:30:33 +01:00
// Build the hyper client from the HTTPS connector.
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
2021-11-30 07:29:41 +01:00
let mut builder = Request::get(uri);
// Copy useful headers from original request
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
2021-05-20 21:24:06 +02:00
if let Some(value) = req.headers().get(key) {
builder = builder.header(key, value);
}
}
2021-05-20 21:24:06 +02:00
let stream_request = builder.body(Body::empty()).map_err(|_| "Couldn't build empty body in stream".to_string())?;
2021-03-17 23:30:33 +01:00
client
.request(stream_request)
2021-03-17 23:30:33 +01:00
.await
.map(|mut res| {
let mut rm = |key: &str| res.headers_mut().remove(key);
rm("access-control-expose-headers");
rm("server");
rm("vary");
rm("etag");
rm("x-cdn");
rm("x-cdn-client-region");
rm("x-cdn-name");
rm("x-cdn-server-region");
rm("x-reddit-cdn");
rm("x-reddit-video-features");
2021-03-17 23:30:33 +01:00
res
})
.map_err(|e| e.to_string())
}
fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String>> {
2021-03-17 23:30:33 +01:00
// Prepare the HTTPS connector.
2021-11-22 07:44:05 +01:00
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_or_http().enable_http1().build();
2021-03-17 23:30:33 +01:00
2021-05-20 21:24:06 +02:00
// Construct the hyper client from the HTTPS connector.
2021-03-17 23:30:33 +01:00
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
2021-05-20 21:24:06 +02:00
// Build request
2021-03-18 05:26:06 +01:00
let builder = Request::builder()
2021-03-18 05:40:55 +01:00
.method("GET")
.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("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive")
.header("Cookie", if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%7D" } else { "" })
2021-03-18 05:40:55 +01:00
.body(Body::empty());
2021-03-17 23:30:33 +01:00
async move {
2021-03-18 05:26:06 +01:00
match builder {
Ok(req) => match client.request(req).await {
Ok(response) => {
if response.status().to_string().starts_with('3') {
request(
response
.headers()
.get("Location")
2021-11-30 07:29:41 +01:00
.map(|val| {
let new_url = percent_encode(val.as_bytes(), CONTROLS).to_string();
format!("{}{}raw_json=1", new_url, if new_url.contains('?') { "&" } else { "?" })
2021-11-30 07:29:41 +01:00
})
2021-03-18 05:26:06 +01:00
.unwrap_or_default()
.to_string(),
quarantine,
2021-03-18 05:26:06 +01:00
)
.await
} else {
Ok(response)
}
2021-03-17 23:30:33 +01:00
}
2021-03-18 05:26:06 +01:00
Err(e) => Err(e.to_string()),
},
2021-03-18 05:40:55 +01:00
Err(_) => Err("Post url contains non-ASCII characters".to_string()),
2021-03-17 23:30:33 +01:00
}
}
.boxed()
}
// Make a request to a Reddit API and parse the JSON response
#[cached(size = 100, time = 30, result = true)]
pub async fn json(path: String, quarantine: bool) -> Result<Value, String> {
2021-03-17 23:30:33 +01:00
// Build Reddit url from path
let url = format!("https://www.reddit.com{}", path);
// Closure to quickly build errors
let err = |msg: &str, e: String| -> Result<Value, String> {
2021-03-18 05:26:06 +01:00
// eprintln!("{} - {}: {}", url, msg, e);
Err(format!("{}: {}", msg, e))
2021-03-17 23:30:33 +01:00
};
// Fetch the url...
match request(url.clone(), quarantine).await {
2021-03-17 23:30:33 +01:00
Ok(response) => {
let status = response.status();
2021-03-17 23:30:33 +01:00
// asynchronously aggregate the chunks of the body
match hyper::body::aggregate(response).await {
Ok(body) => {
// Parse the response from Reddit as JSON
match serde_json::from_reader(body.reader()) {
Ok(value) => {
let json: Value = value;
// If Reddit returned an error
if json["error"].is_i64() {
Err(
json["reason"]
.as_str()
.unwrap_or_else(|| {
json["message"].as_str().unwrap_or_else(|| {
eprintln!("{} - Error parsing reddit error", url);
"Error parsing reddit error"
})
})
.to_string(),
)
} else {
Ok(json)
}
}
Err(e) => {
if status.is_server_error() {
Err("Reddit is having issues, check if there's an outage".to_string())
} else {
err("Failed to parse page JSON data", e.to_string())
}
}
2021-03-17 23:30:33 +01:00
}
}
2021-03-18 05:26:06 +01:00
Err(e) => err("Failed receiving body from Reddit", e.to_string()),
2021-03-17 23:30:33 +01:00
}
}
2021-03-27 04:00:47 +01:00
Err(e) => err("Couldn't send request to Reddit", e),
2021-03-17 23:30:33 +01:00
}
}