Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move from Actix Web to Tide #99

Merged
merged 17 commits into from Feb 9, 2021
Merged
1,710 changes: 892 additions & 818 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Cargo.toml
Expand Up @@ -8,15 +8,15 @@ authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2018"

[dependencies]
tide = "0.16"
async-std = { version = "1", features = ["attributes"] }
surf = "2"
base64 = "0.13"
actix-web = { version = "3.3", features = ["rustls"] }
cached = "0.23"
futures = "0.3"
askama = "0.10"
ureq = "2"
serde = { version = "1.0", default_features = false, features = ["derive"] }
serde_json = "1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-recursion = "0.3"
url = "2.2"
regex = "1.4"
regex = "1"
time = "0.2"
cached = "0.23"
2 changes: 1 addition & 1 deletion rustfmt.toml
@@ -1,4 +1,4 @@
edition = "2018"
tab_spaces = 2
hard_tabs = true
max_width = 175
max_width = 150
295 changes: 174 additions & 121 deletions src/main.rs
@@ -1,9 +1,7 @@
// Import Crates
use actix_web::{
dev::{Service, ServiceResponse},
middleware, web, App, HttpResponse, HttpServer,
};
use futures::future::FutureExt;
// use askama::filters::format;
use surf::utils::async_trait;
use tide::{utils::After, Middleware, Next, Request, Response};

// Reference local files
mod post;
Expand All @@ -14,43 +12,103 @@ mod subreddit;
mod user;
mod utils;

// Build middleware
struct HttpsRedirect<HttpsOnly>(HttpsOnly);
struct NormalizePath;

#[async_trait]
impl<State, HttpsOnly> Middleware<State> for HttpsRedirect<HttpsOnly>
where
State: Clone + Send + Sync + 'static,
HttpsOnly: Into<bool> + Copy + Send + Sync + 'static,
{
async fn handle(&self, request: Request<State>, next: Next<'_, State>) -> tide::Result {
let secure = request.url().scheme() == "https";

if self.0.into() && !secure {
let mut secured = request.url().to_owned();
secured.set_scheme("https").unwrap_or_default();

Ok(Response::builder(302).header("Location", secured.to_string()).build())
} else {
Ok(next.run(request).await)
}
}
}

#[async_trait]
impl<State: Clone + Send + Sync + 'static> Middleware<State> for NormalizePath {
async fn handle(&self, request: Request<State>, next: Next<'_, State>) -> tide::Result {
if !request.url().path().ends_with('/') {
Ok(Response::builder(301).header("Location", format!("{}/", request.url().path())).build())
} else {
Ok(next.run(request).await)
}
}
}

// Create Services
async fn style() -> HttpResponse {
HttpResponse::Ok().content_type("text/css").body(include_str!("../static/style.css"))
async fn style(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("text/css")
.body(include_str!("../static/style.css"))
.build(),
)
}

// Required for creating a PWA
async fn manifest() -> HttpResponse {
HttpResponse::Ok().content_type("application/json").body(include_str!("../static/manifest.json"))
async fn manifest(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("application/json")
.body(include_str!("../static/manifest.json"))
.build(),
)
}

// Required for the manifest to be valid
async fn pwa_logo() -> HttpResponse {
HttpResponse::Ok().content_type("image/png").body(include_bytes!("../static/logo.png").as_ref())
async fn pwa_logo(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("image/png")
.body(include_bytes!("../static/logo.png").as_ref())
.build(),
)
}

// Required for iOS App Icons
async fn iphone_logo() -> HttpResponse {
HttpResponse::Ok()
.content_type("image/png")
.body(include_bytes!("../static/touch-icon-iphone.png").as_ref())
async fn iphone_logo(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("image/png")
.body(include_bytes!("../static/touch-icon-iphone.png").as_ref())
.build(),
)
}

async fn robots() -> HttpResponse {
HttpResponse::Ok()
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body("User-agent: *\nAllow: /")
async fn robots(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("text/plain")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body("User-agent: *\nAllow: /")
.build(),
)
}

async fn favicon() -> HttpResponse {
HttpResponse::Ok()
.content_type("image/x-icon")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(include_bytes!("../static/favicon.ico").as_ref())
async fn favicon(_req: Request<()>) -> tide::Result {
Ok(
Response::builder(200)
.content_type("image/vnd.microsoft.icon")
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
.body(include_bytes!("../static/favicon.ico").as_ref())
.build(),
)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[async_std::main]
async fn main() -> tide::Result<()> {
let mut address = "0.0.0.0:8080".to_string();
let mut force_https = false;

Expand All @@ -62,101 +120,96 @@ async fn main() -> std::io::Result<()> {
}
}

// start http server
// Start HTTP server
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);

HttpServer::new(move || {
App::new()
// Redirect to HTTPS if "--redirect-https" enabled
.wrap_fn(move |req, srv| {
let secure = req.connection_info().scheme() == "https";
let https_url = format!("https://{}{}", req.connection_info().host(), req.uri().to_string());
srv.call(req).map(move |res: Result<ServiceResponse, _>| {
if force_https && !secure {
Ok(ServiceResponse::new(
res.unwrap().request().to_owned(),
HttpResponse::Found().header("Location", https_url).finish(),
))
} else {
res
}
})
})
// Append trailing slash and remove double slashes
.wrap(middleware::NormalizePath::default())
// Apply default headers for security
.wrap(
middleware::DefaultHeaders::new()
.header("Referrer-Policy", "no-referrer")
.header("X-Content-Type-Options", "nosniff")
.header("X-Frame-Options", "DENY")
.header(
"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';",
),
)
// Default service in case no routes match
.default_service(web::get().to(|| utils::error("Nothing here".to_string())))
// Read static files
.route("/style.css/", web::get().to(style))
.route("/favicon.ico/", web::get().to(favicon))
.route("/robots.txt/", web::get().to(robots))
.route("/manifest.json/", web::get().to(manifest))
.route("/logo.png/", web::get().to(pwa_logo))
.route("/touch-icon-iphone.png/", web::get().to(iphone_logo))
// Proxy media through Libreddit
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
// Browse user profile
.service(
web::scope("/{scope:user|u}").service(
web::scope("/{username}").route("/", web::get().to(user::profile)).service(
web::scope("/comments/{id}/{title}")
.route("/", web::get().to(post::item))
.route("/{comment_id}/", web::get().to(post::item)),
),
),
)
// Configure settings
.service(web::resource("/settings/").route(web::get().to(settings::get)).route(web::post().to(settings::set)))
// Subreddit services
.service(
web::scope("/r/{sub}")
// See posts and info about subreddit
.route("/", web::get().to(subreddit::page))
.route("/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
// Handle subscribe/unsubscribe
.route("/{action:subscribe|unsubscribe}/", web::post().to(subreddit::subscriptions))
// View post on subreddit
.service(
web::scope("/comments/{id}/{title}")
.route("/", web::get().to(post::item))
.route("/{comment_id}/", web::get().to(post::item)),
)
// Search inside subreddit
.route("/search/", web::get().to(search::find))
// View wiki of subreddit
.service(
web::scope("/{scope:wiki|w}")
.route("/", web::get().to(subreddit::wiki))
.route("/{page}/", web::get().to(subreddit::wiki)),
),
)
// Front page
.route("/", web::get().to(subreddit::page))
.route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
// View Reddit wiki
.service(
web::scope("/wiki")
.route("/", web::get().to(subreddit::wiki))
.route("/{page}/", web::get().to(subreddit::wiki)),
)
// Search all of Reddit
.route("/search/", web::get().to(search::find))
// Short link for post
.route("/{id:.{5,6}}/", web::get().to(post::item))
})
.bind(&address)
.unwrap_or_else(|e| panic!("Cannot bind to the address {}: {}", address, e))
.run()
.await
let mut app = tide::new();

// Redirect to HTTPS if "--redirect-https" enabled
app.with(HttpsRedirect(force_https));

// Append trailing slash and remove double slashes
app.with(NormalizePath);

// Apply default headers for security
app.with(After(|mut res: Response| async move {
res.insert_header("Referrer-Policy", "no-referrer");
res.insert_header("X-Content-Type-Options", "nosniff");
res.insert_header("X-Frame-Options", "DENY");
res.insert_header(
"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';",
);
Ok(res)
}));

// Read static files
app.at("/style.css/").get(style);
app.at("/favicon.ico/").get(favicon);
app.at("/robots.txt/").get(robots);
app.at("/manifest.json/").get(manifest);
app.at("/logo.png/").get(pwa_logo);
app.at("/touch-icon-iphone.png/").get(iphone_logo);

// Proxy media through Libreddit
app.at("/proxy/*url/").get(proxy::handler);

// Browse user profile
app.at("/u/:name/").get(user::profile);
app.at("/u/:name/comments/:id/:title/").get(post::item);
app.at("/u/:name/comments/:id/:title/:comment/").get(post::item);

app.at("/user/:name/").get(user::profile);
app.at("/user/:name/comments/:id/:title/").get(post::item);
app.at("/user/:name/comments/:id/:title/:comment/").get(post::item);

// Configure settings
app.at("/settings/").get(settings::get).post(settings::set);

// Subreddit services
// See posts and info about subreddit
app.at("/r/:sub/").get(subreddit::page);
// Handle subscribe/unsubscribe
app.at("/r/:sub/subscribe/").post(subreddit::subscriptions);
app.at("/r/:sub/unsubscribe/").post(subreddit::subscriptions);
// View post on subreddit
app.at("/r/:sub/comments/:id/:title/").get(post::item);
app.at("/r/:sub/comments/:id/:title/:comment_id/").get(post::item);
// Search inside subreddit
app.at("/r/:sub/search/").get(search::find);
// View wiki of subreddit
app.at("/r/:sub/w/").get(subreddit::wiki);
app.at("/r/:sub/w/:page/").get(subreddit::wiki);
app.at("/r/:sub/wiki/").get(subreddit::wiki);
app.at("/r/:sub/wiki/:page/").get(subreddit::wiki);
// Sort subreddit posts
app.at("/r/:sub/:sort/").get(subreddit::page);

// Front page
app.at("/").get(subreddit::page);

// View Reddit wiki
app.at("/w/").get(subreddit::wiki);
app.at("/w/:page/").get(subreddit::wiki);
app.at("/wiki/").get(subreddit::wiki);
app.at("/wiki/:page/").get(subreddit::wiki);

// Search all of Reddit
app.at("/search/").get(search::find);

// Short link for post
// .route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
// .route("/{id:.{5,6}}/", web::get().to(post::item))
app.at("/:id/").get(|req: Request<()>| async {
match req.param("id").unwrap_or_default() {
"best" | "hot" | "new" | "top" | "rising" | "controversial" => subreddit::page(req).await,
_ => post::item(req).await,
}
});

// Default service in case no routes match
app.at("*").get(|_| utils::error("Nothing here".to_string()));

app.listen("127.0.0.1:8080").await?;
Ok(())
}