redsunlib/src/main.rs

243 lines
8.0 KiB
Rust
Raw Normal View History

2021-03-09 15:49:06 +13:00
// Global specifiers
#![forbid(unsafe_code)]
#![warn(clippy::pedantic, clippy::all)]
2021-03-09 16:22:10 +13:00
#![allow(
2021-03-10 19:13:46 +13:00
clippy::needless_pass_by_value,
2021-03-09 16:22:10 +13:00
clippy::match_wildcard_for_single_variants,
clippy::cast_possible_truncation,
2021-03-10 19:13:46 +13:00
clippy::similar_names,
clippy::cast_possible_wrap
2021-03-09 16:22:10 +13:00
)]
2021-03-09 15:49:06 +13:00
2020-10-26 09:25:59 +13:00
// Reference local files
mod post;
2021-01-01 12:54:13 +13:00
mod search;
2021-01-06 15:04:49 +13:00
mod settings;
2020-10-26 09:25:59 +13:00
mod subreddit;
2020-10-26 16:57:19 +13:00
mod user;
2020-11-26 10:53:30 +13:00
mod utils;
2020-10-26 09:25:59 +13:00
2021-02-14 12:02:38 +13:00
// Import Crates
2021-03-18 11:30:33 +13:00
use clap::{App as cli, Arg};
2021-02-14 12:02:38 +13:00
2021-03-18 11:30:33 +13:00
use futures_lite::FutureExt;
use hyper::{header::HeaderValue, Body, Request, Response};
2021-03-18 11:30:33 +13:00
mod client;
use client::proxy;
use server::RequestExt;
use utils::{error, redirect};
mod server;
2020-10-26 09:25:59 +13:00
// Create Services
// Required for the manifest to be valid
2021-03-18 11:30:33 +13:00
async fn pwa_logo() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "image/png")
.body(include_bytes!("../static/logo.png").as_ref().into())
.unwrap_or_default(),
)
}
// Required for iOS App Icons
2021-03-18 11:30:33 +13:00
async fn iphone_logo() -> Result<Response<Body>, String> {
Ok(
2021-03-18 11:30:33 +13:00
Response::builder()
.status(200)
.header("content-type", "image/png")
.body(include_bytes!("../static/apple-touch-icon.png").as_ref().into())
.unwrap_or_default(),
)
}
2021-03-18 11:30:33 +13:00
async fn favicon() -> Result<Response<Body>, String> {
Ok(
2021-03-18 11:30:33 +13:00
Response::builder()
.status(200)
.header("content-type", "image/vnd.microsoft.icon")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
2021-03-18 11:30:33 +13:00
.body(include_bytes!("../static/favicon.ico").as_ref().into())
.unwrap_or_default(),
)
2020-10-26 09:25:59 +13:00
}
2021-03-18 11:30:33 +13:00
async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Response<Body>, String> {
let mut res = Response::builder()
.status(200)
.header("content-type", content_type)
.body(body.to_string().into())
.unwrap_or_default();
2021-02-25 06:26:01 +13:00
if cache {
match HeaderValue::from_str("public, max-age=1209600, s-maxage=86400") {
Ok(val) => {
res.headers_mut().insert("Cache-Control", val);
}
Err(_) => (),
}
2021-02-25 06:26:01 +13:00
}
Ok(res)
}
2021-03-18 11:30:33 +13:00
#[tokio::main]
async fn main() {
let matches = cli::new("Libreddit")
2021-02-19 08:40:10 +13:00
.version(env!("CARGO_PKG_VERSION"))
.about("Private front-end for Reddit written in Rust ")
.arg(
Arg::with_name("address")
.short("a")
.long("address")
.value_name("ADDRESS")
.help("Sets address to listen on")
2021-02-19 08:49:50 +13:00
.default_value("0.0.0.0")
.takes_value(true),
)
.arg(
Arg::with_name("port")
.short("p")
.long("port")
.value_name("PORT")
.help("Port to listen on")
.default_value("8080")
2021-02-19 08:40:10 +13:00
.takes_value(true),
)
.arg(
Arg::with_name("redirect-https")
.short("r")
.long("redirect-https")
2021-03-18 11:30:33 +13:00
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
2021-02-19 08:40:10 +13:00
.takes_value(false),
)
.get_matches();
2021-02-19 08:49:50 +13:00
let address = matches.value_of("address").unwrap_or("0.0.0.0");
let port = matches.value_of("port").unwrap_or("8080");
2021-03-18 11:30:33 +13:00
let _force_https = matches.is_present("redirect-https");
2021-02-19 08:40:10 +13:00
2021-02-19 08:49:50 +13:00
let listener = format!("{}:{}", address, port);
2020-11-23 16:21:07 +13:00
2021-02-25 08:00:04 +13:00
println!("Starting Libreddit...");
2021-03-18 11:30:33 +13:00
// Begin constructing a server
let mut app = server::Server::new();
2021-03-18 11:30:33 +13:00
// Define default headers (added to all responses)
app.default_headers = headers! {
"Referrer-Policy" => "no-referrer",
"X-Content-Type-Options" => "nosniff",
"X-Frame-Options" => "DENY",
"Content-Security-Policy" => "default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';"
};
// Read static files
2021-03-18 11:30:33 +13:00
app.at("/style.css").get(|_| resource(include_str!("../static/style.css"), "text/css", false).boxed());
2021-02-26 06:07:45 +13:00
app
2021-03-18 11:30:33 +13:00
.at("/manifest.json")
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
app.at("/robots.txt").get(|_| resource("User-agent: *\nAllow: /", "text/plain", true).boxed());
app.at("/favicon.ico").get(|_| favicon().boxed());
app.at("/logo.png").get(|_| pwa_logo().boxed());
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
// Proxy media through Libreddit
2021-03-18 11:30:33 +13:00
app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed());
app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed());
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
app.at("/preview/:loc/:id/:query").get(|r| proxy(r, "https://{loc}view.redd.it/{id}?{query}").boxed());
app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed());
app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed());
// Browse user profile
2021-03-18 11:30:33 +13:00
app
.at("/u/:name")
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
app.at("/u/:name/comments/:id/:title").get(|r| post::item(r).boxed());
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
2021-03-18 17:40:55 +13:00
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed());
2021-03-18 11:30:33 +13:00
app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
app.at("/user/:name/comments/:id/:title").get(|r| post::item(r).boxed());
app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
// Configure settings
2021-03-18 11:30:33 +13:00
app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed());
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
// Subreddit services
2021-03-18 11:30:33 +13:00
app.at("/r/:sub").get(|r| subreddit::community(r).boxed());
2021-02-25 06:26:01 +13:00
2021-03-20 18:04:44 +13:00
app
.at("/r/u_:name")
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
2021-03-18 11:30:33 +13:00
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions(r).boxed());
2021-02-25 06:26:01 +13:00
2021-03-18 11:30:33 +13:00
app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed());
app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed());
app.at("/r/:sub/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
2021-02-25 06:26:01 +13:00
2021-03-18 11:30:33 +13:00
app.at("/r/:sub/search").get(|r| search::find(r).boxed());
2021-02-25 06:26:01 +13:00
2021-03-18 17:40:55 +13:00
app
.at("/r/:sub/w")
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki", r.param("sub").unwrap_or_default()))) }.boxed());
app
.at("/r/:sub/w/:page")
.get(|r| async move { Ok(redirect(format!("/r/{}/wiki/{}", r.param("sub").unwrap_or_default(), r.param("wiki").unwrap_or_default()))) }.boxed());
app.at("/r/:sub/wiki").get(|r| subreddit::wiki(r).boxed());
2021-03-18 11:30:33 +13:00
app.at("/r/:sub/wiki/:page").get(|r| subreddit::wiki(r).boxed());
2021-02-25 06:26:01 +13:00
2021-03-18 11:30:33 +13:00
app.at("/r/:sub/:sort").get(|r| subreddit::community(r).boxed());
2021-03-06 03:24:40 +13:00
// Comments handler
2021-03-18 17:40:55 +13:00
app.at("/comments/:id").get(|r| post::item(r).boxed());
2021-03-06 03:24:40 +13:00
// Front page
2021-03-18 11:30:33 +13:00
app.at("/").get(|r| subreddit::community(r).boxed());
// View Reddit wiki
2021-03-18 17:40:55 +13:00
app.at("/w").get(|_| async move { Ok(redirect("/wiki".to_string())) }.boxed());
app
.at("/w/:page")
.get(|r| async move { Ok(redirect(format!("/wiki/{}", r.param("page").unwrap_or_default()))) }.boxed());
2021-03-18 11:30:33 +13:00
app.at("/wiki").get(|r| subreddit::wiki(r).boxed());
app.at("/wiki/:page").get(|r| subreddit::wiki(r).boxed());
// Search all of Reddit
2021-03-18 11:30:33 +13:00
app.at("/search").get(|r| search::find(r).boxed());
2021-02-22 07:13:20 +13:00
// Handle about pages
2021-03-18 11:30:33 +13:00
app.at("/about").get(|req| error(req, "About pages aren't added yet".to_string()).boxed());
2021-03-20 18:04:44 +13:00
app.at("/:id").get(|req: Request<Body>| match req.param("id").as_deref() {
// Sort front page
Some("best") | Some("hot") | Some("new") | Some("top") | Some("rising") | Some("controversial") => subreddit::community(req).boxed(),
// Short link for post
Some(id) if id.len() > 4 && id.len() < 7 => post::item(req).boxed(),
// Error message for unknown pages
_ => error(req, "Nothing here".to_string()).boxed(),
});
// Default service in case no routes match
2021-03-18 11:30:33 +13:00
app.at("/*").get(|req| error(req, "Nothing here".to_string()).boxed());
2021-02-25 08:00:04 +13:00
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), listener);
2021-03-18 11:30:33 +13:00
let server = app.listen(listener);
2021-02-25 06:26:01 +13:00
2021-03-18 11:30:33 +13:00
// Run this server for... forever!
if let Err(e) = server.await {
eprintln!("Server error: {}", e);
}
2020-10-26 16:57:19 +13:00
}