mirror of https://github.com/spikecodes/libreddit
89 lines
3.0 KiB
Rust
89 lines
3.0 KiB
Rust
use async_std::{io, net::TcpStream, prelude::*};
|
|
use async_tls::TlsConnector;
|
|
use tide::{http::url::Url, Request, Response};
|
|
|
|
/// Handle tide routes to proxy by parsing `params` from `req`uest.
|
|
pub async fn handler(req: Request<()>, format: &str, params: Vec<&str>) -> tide::Result {
|
|
let mut url = format.to_string();
|
|
|
|
for name in params {
|
|
let param = req.param(name).unwrap_or_default();
|
|
url = url.replacen("{}", param, 1);
|
|
}
|
|
|
|
request(url).await
|
|
}
|
|
|
|
/// Sends a request to a Reddit media domain and proxy the response.
|
|
///
|
|
/// Relays the `Content-Length` and `Content-Type` header.
|
|
async fn request(url: String) -> tide::Result {
|
|
// Parse url into parts
|
|
let parts = Url::parse(&url)?;
|
|
let host = parts.host().map(|host| host.to_string()).unwrap_or_default();
|
|
let domain = parts.domain().unwrap_or_default();
|
|
let path = format!("{}?{}", parts.path(), parts.query().unwrap_or_default());
|
|
// Build reddit-compliant user agent for Libreddit
|
|
let user_agent = format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"));
|
|
|
|
// Construct a request body
|
|
let req = format!(
|
|
"GET {} HTTP/1.1\r\nHost: {}\r\nAccept: */*\r\nConnection: close\r\nUser-Agent: {}\r\n\r\n",
|
|
path, host, user_agent
|
|
);
|
|
|
|
// Initialize TLS connector for requests
|
|
let connector = TlsConnector::default();
|
|
|
|
// Open a TCP connection
|
|
let tcp_stream = TcpStream::connect(format!("{}:443", domain)).await?;
|
|
|
|
// Use the connector to start the handshake process
|
|
let mut tls_stream = connector.connect(domain, tcp_stream).await?;
|
|
|
|
// Write the aforementioned HTTP request to the stream
|
|
tls_stream.write_all(req.as_bytes()).await?;
|
|
|
|
// And read the response
|
|
let mut writer = Vec::new();
|
|
io::copy(&mut tls_stream, &mut writer).await?;
|
|
|
|
// Find the delimiter which separates the body and headers
|
|
match (0..writer.len()).find(|i| writer[i.to_owned()] == 10_u8 && writer[i - 2] == 10_u8) {
|
|
Some(delim) => {
|
|
// Split the response into the body and headers
|
|
let split = writer.split_at(delim);
|
|
let headers_str = String::from_utf8_lossy(split.0);
|
|
let headers = headers_str.split("\r\n").collect::<Vec<&str>>();
|
|
let body = split.1[1..split.1.len()].to_vec();
|
|
|
|
// Parse the status code from the first header line
|
|
let status: u16 = headers[0].split(' ').collect::<Vec<&str>>()[1].parse().unwrap_or_default();
|
|
|
|
// Define a closure for easier header fetching
|
|
let header = |name: &str| {
|
|
headers
|
|
.iter()
|
|
.find(|x| x.starts_with(name))
|
|
.map(|f| f.split(": ").collect::<Vec<&str>>()[1])
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
// Parse Content-Length and Content-Type from headers
|
|
let content_length = header("Content-Length");
|
|
let content_type = header("Content-Type");
|
|
|
|
// Build response
|
|
Ok(
|
|
Response::builder(status)
|
|
.body(tide::http::Body::from_bytes(body))
|
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
|
.header("Content-Length", content_length)
|
|
.header("Content-Type", content_type)
|
|
.build(),
|
|
)
|
|
}
|
|
None => Ok(Response::builder(503).body("Couldn't parse media".to_string()).build()),
|
|
}
|
|
}
|