Restrict Proxy to Reddit Domains

This commit is contained in:
spikecodes 2021-01-02 20:50:23 -08:00
parent f49bff9853
commit 5ea504e6e8
14 changed files with 156 additions and 61 deletions

View File

@ -7,13 +7,9 @@ version = "0.2.5"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018"
[features]
default = ["proxy"]
proxy = ["actix-web/rustls", "base64"]
[dependencies]
base64 = { version = "0.13.0", optional = true }
actix-web = "3.2.0"
base64 = "0.13.0"
actix-web = { version = "3.2.0", features = ["rustls"] }
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
askama = "0.8.0"
serde = "1.0.117"

View File

@ -5,6 +5,7 @@ use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServ
mod post;
mod proxy;
mod search;
// mod settings;
mod subreddit;
mod user;
mod utils;
@ -50,6 +51,9 @@ async fn main() -> std::io::Result<()> {
.route("/style.css/", web::get().to(style))
.route("/favicon.ico/", web::get().to(HttpResponse::Ok))
.route("/robots.txt/", web::get().to(robots))
// SETTINGS SERVICE
// .route("/settings/", web::get().to(settings::get))
// .route("/settings/save/", web::post().to(settings::set))
// PROXY SERVICE
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
// SEARCH SERVICES

View File

@ -16,7 +16,7 @@ struct PostTemplate {
sort: String,
}
pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
pub async fn item(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
let sort = param(&path, "sort");
let id = req.match_info().get("id").unwrap_or("").to_string();
@ -35,7 +35,7 @@ pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
// Use the Post and Comment structs to generate a website to show users
let s = PostTemplate { comments, post, sort }.render().unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
HttpResponse::Ok().content_type("text/html").body(s)
}
// If the Reddit API returns an error, exit and send error page to user
Err(msg) => error(msg.to_string()).await,

View File

@ -1,30 +1,40 @@
use actix_web::{client::Client, web, Error, HttpResponse, Result};
use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
use url::Url;
#[cfg(feature = "proxy")]
use base64::decode;
pub async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> {
if cfg!(feature = "proxy") {
#[cfg(feature = "proxy")]
let media: String;
pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
let domains = vec![
"a.thumbs.redditmedia.com",
"b.thumbs.redditmedia.com",
"preview.redd.it",
"external-preview.redd.it",
"i.redd.it",
"v.redd.it",
];
#[cfg(not(feature = "proxy"))]
let media = url;
match decode(b64) {
Ok(bytes) => {
let media = String::from_utf8(bytes).unwrap();
#[cfg(feature = "proxy")]
match decode(url) {
Ok(bytes) => media = String::from_utf8(bytes).unwrap(),
Err(_e) => return Ok(HttpResponse::Ok().body("")),
};
match Url::parse(media.as_str()) {
Ok(url) => {
let domain = url.domain().unwrap_or_default();
let client = Client::default();
client
.get(media.replace("&amp;", "&"))
.send()
.await
.map_err(Error::from)
.map(|res| HttpResponse::build(res.status()).streaming(res))
} else {
Ok(HttpResponse::Ok().body(""))
if domains.contains(&domain) {
Client::default()
.get(media.replace("&amp;", "&"))
.send()
.await
.map_err(Error::from)
.map(|res| HttpResponse::build(res.status()).streaming(res))
} else {
Err(error::ErrorForbidden("Resource must be from Reddit"))
}
}
Err(_) => Err(error::ErrorBadRequest("Can't parse encoded base64 URL")),
}
}
Err(_) => Err(error::ErrorBadRequest("Can't decode base64 URL")),
}
}

View File

@ -1,11 +1,10 @@
// CRATES
use crate::utils::{error, fetch_posts, param, Post};
use actix_web::{HttpRequest, HttpResponse, Result};
use actix_web::{HttpRequest, HttpResponse};
use askama::Template;
// STRUCTS
#[derive(Template)]
#[allow(dead_code)]
#[template(path = "search.html", escape = "none")]
struct SearchTemplate {
posts: Vec<Post>,
@ -16,7 +15,7 @@ struct SearchTemplate {
}
// SERVICES
pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
pub async fn find(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}", req.path(), req.query_string());
let q = param(&path, "q");
let sort = if param(&path, "sort").is_empty() {
@ -27,8 +26,8 @@ pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
let sub = req.match_info().get("sub").unwrap_or("").to_string();
match fetch_posts(&path, String::new()).await {
Ok(posts) => {
let s = SearchTemplate {
Ok(posts) => HttpResponse::Ok().content_type("text/html").body(
SearchTemplate {
posts: posts.0,
query: q,
sub,
@ -36,9 +35,8 @@ pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
ends: (param(&path, "after"), posts.1),
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
}
.unwrap(),
),
Err(msg) => error(msg.to_string()).await,
}
}

48
src/settings.rs Normal file
View File

@ -0,0 +1,48 @@
// // CRATES
// use crate::utils::cookies;
// use actix_web::{cookie::Cookie, web::Form, HttpRequest, HttpResponse, Result}; // http::Method,
// use askama::Template;
// // STRUCTS
// #[derive(Template)]
// #[template(path = "settings.html", escape = "none")]
// struct SettingsTemplate {
// pref_nsfw: String,
// }
// #[derive(serde::Deserialize)]
// pub struct Preferences {
// pref_nsfw: Option<String>,
// }
// // FUNCTIONS
// // Retrieve cookies from request "Cookie" header
// pub async fn get(req: HttpRequest) -> Result<HttpResponse> {
// let cookies = cookies(req);
// let pref_nsfw: String = cookies.get("pref_nsfw").unwrap_or(&String::new()).to_owned();
// let s = SettingsTemplate { pref_nsfw }.render().unwrap();
// Ok(HttpResponse::Ok().content_type("text/html").body(s))
// }
// // Set cookies using response "Set-Cookie" header
// pub async fn set(form: Form<Preferences>) -> HttpResponse {
// let nsfw: Cookie = match &form.pref_nsfw {
// Some(value) => Cookie::build("pref_nsfw", value).path("/").secure(true).http_only(true).finish(),
// None => Cookie::build("pref_nsfw", "").finish(),
// };
// let body = SettingsTemplate {
// pref_nsfw: form.pref_nsfw.clone().unwrap_or_default(),
// }
// .render()
// .unwrap();
// HttpResponse::Found()
// .content_type("text/html")
// .set_header("Set-Cookie", nsfw.to_string())
// .set_header("Location", "/settings")
// .body(body)
// }

View File

@ -22,7 +22,7 @@ struct WikiTemplate {
}
// SERVICES
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
pub async fn page(req: HttpRequest) -> HttpResponse {
let path = format!("{}.json?{}", req.path(), req.query_string());
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
@ -43,13 +43,13 @@ pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
HttpResponse::Ok().content_type("text/html").body(s)
}
Err(msg) => error(msg.to_string()).await,
}
}
pub async fn wiki(req: HttpRequest) -> Result<HttpResponse> {
pub async fn wiki(req: HttpRequest) -> HttpResponse {
let sub = req.match_info().get("sub").unwrap_or("reddit.com");
let page = req.match_info().get("page").unwrap_or("index");
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
@ -63,7 +63,7 @@ pub async fn wiki(req: HttpRequest) -> Result<HttpResponse> {
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
HttpResponse::Ok().content_type("text/html").body(s)
}
Err(msg) => error(msg.to_string()).await,
}

View File

@ -14,7 +14,8 @@ struct UserTemplate {
ends: (String, String),
}
pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
// FUNCTIONS
pub async fn profile(req: HttpRequest) -> HttpResponse {
// Build the Reddit JSON API path
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
@ -36,7 +37,7 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
}
.render()
.unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
HttpResponse::Ok().content_type("text/html").body(s)
}
// If there is an error show error page
Err(msg) => error(msg.to_string()).await,

View File

@ -1,17 +1,17 @@
// use std::collections::HashMap;
//
// CRATES
//
use actix_web::{http::StatusCode, HttpResponse, Result};
use actix_web::{HttpResponse, Result};
use askama::Template;
use base64::encode;
use chrono::{TimeZone, Utc};
use regex::Regex;
use serde_json::from_str;
use url::Url;
// use surf::{client, get, middleware::Redirect};
#[cfg(feature = "proxy")]
use base64::encode;
//
// STRUCTS
//
@ -102,17 +102,31 @@ pub fn param(path: &str, value: &str) -> String {
pairs.get(value).unwrap_or(&String::new()).to_owned()
}
// Cookies from request
// pub fn cookies(req: HttpRequest) -> HashMap<String, String> {
// let mut result: HashMap<String, String> = HashMap::new();
// let cookies: Vec<Cookie> = req
// .headers()
// .get_all("Cookie")
// .map(|value| value.to_str().unwrap())
// .map(|unparsed| Cookie::parse(unparsed).unwrap())
// .collect();
// for cookie in cookies {
// result.insert(cookie.name().to_string(), cookie.value().to_string());
// }
// result
// }
// Direct urls to proxy if proxy is enabled
pub fn format_url(url: String) -> String {
if url.is_empty() {
return String::new();
};
#[cfg(feature = "proxy")]
return "/proxy/".to_string() + encode(url).as_str();
#[cfg(not(feature = "proxy"))]
return url.to_string();
format!("/proxy/{}", encode(url).as_str())
}
// Rewrite Reddit links to Libreddit in body of text
@ -217,10 +231,10 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
// NETWORKING
//
pub async fn error(message: String) -> Result<HttpResponse> {
pub async fn error(message: String) -> HttpResponse {
let msg = if message.is_empty() { "Page not found".to_string() } else { message };
let body = ErrorTemplate { message: msg }.render().unwrap_or_default();
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(body))
HttpResponse::NotFound().content_type("text/html").body(body)
}
// Make a request to a Reddit API and parse the JSON response

View File

@ -599,8 +599,15 @@ td, th {
}
@media screen and (max-width: 800px) {
main { flex-direction: column-reverse; }
nav { flex-direction: column; }
main {
flex-direction: column-reverse;
padding: 10px;
margin: 10px 0;
}
nav {
flex-direction: column;
margin: 10px;
}
aside, #subreddit, #user {
margin: 0;

View File

@ -4,8 +4,7 @@
{% block head %}
<title>{% block title %}Libreddit{% endblock %}</title>
<meta http-equiv="Referrer-Policy" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';
{% if cfg!(not(feature = "proxy")) %}img-src https://*; media-src https://*{% endif %}">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@ -9,7 +9,7 @@
<input id="search" type="text" name="q" placeholder="Search" value="{{ query }}">
{% if sub != "" %}
<div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes">
<input type="checkbox" name="restrict_sr" id="restrict_sr">
<label for="restrict_sr">in r/{{ sub }}</label>
</div>
{% endif %}

18
templates/settings.html Normal file
View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block title %}Libreddit Settings{% endblock %}
{% block search %}
{% call utils::search("".to_owned(), "", "") %}
{% endblock %}
{% block body %}
<main>
<form action="/settings/save" method="POST">
<label for="pref_nsfw">NSFW</label>
<input type="checkbox" name="pref_nsfw" id="pref_nsfw" {% if pref_nsfw == "on" %}checked{% endif %}>
<input id="sort_submit" type="submit" value="&rarr;">
</form>
</main>
{% endblock %}

View File

@ -19,7 +19,7 @@
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
{% if root != "/r/" && !root.is_empty() %}
<div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes">
<input type="checkbox" name="restrict_sr" id="restrict_sr">
<label for="restrict_sr">in {{ root }}</label>
</div>
{% endif %}