diff --git a/src/api/web.rs b/src/api/web.rs index 6619a08d..a6d35efe 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,10 +1,10 @@ use std::io; use std::path::{Path, PathBuf}; +use rocket::http::ContentType; use rocket::request::Request; +use rocket::response::content::{Content, Html}; use rocket::response::{self, NamedFile, Responder}; -use rocket::response::content::Content; -use rocket::http::{ContentType, Status}; use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; @@ -19,57 +19,72 @@ pub fn routes() -> Vec { } } -// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache #[get("/")] -fn web_index() -> WebHeaders> { - web_files("index.html".into()) +fn web_index() -> Cached> { + Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join("index.html"))) } #[get("/app-id.json")] -fn app_id() -> WebHeaders>> { +fn app_id() -> Cached>> { let content_type = ContentType::new("application", "fido.trusted-apps+json"); - WebHeaders(Content(content_type, Json(json!({ - "trustedFacets": [ - { - "version": { "major": 1, "minor": 0 }, - "ids": [ - &CONFIG.domain, - "ios:bundle-id:com.8bit.bitwarden", - "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] - }] - })))) + Cached::long(Content( + content_type, + Json(json!({ + "trustedFacets": [ + { + "version": { "major": 1, "minor": 0 }, + "ids": [ + &CONFIG.domain, + "ios:bundle-id:com.8bit.bitwarden", + "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] + }] + })), + )) } +const ADMIN_PAGE: &'static str = include_str!("../static/admin.html"); + +#[get("/admin")] +fn admin_page() -> Cached> { + Cached::short(Html(ADMIN_PAGE)) +} + +/* // Use this during Admin page development #[get("/admin")] -fn admin_page() -> WebHeaders> { - WebHeaders(NamedFile::open("src/static/admin.html")) // TODO: Change this to embed the page in the binary +fn admin_page() -> Cached> { + Cached::short(NamedFile::open("src/static/admin.html")) } +*/ #[get("/", rank = 1)] // Only match this if the other routes don't match -fn web_files(p: PathBuf) -> WebHeaders> { - WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p))) +fn web_files(p: PathBuf) -> Cached> { + Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p))) } -struct WebHeaders(R); +struct Cached(R, &'static str); + +impl Cached { + fn long(r: R) -> Cached { + // 7 days + Cached(r, "public, max-age=604800".into()) + } -impl<'r, R: Responder<'r>> Responder<'r> for WebHeaders { + fn short(r: R) -> Cached { + // 10 minutes + Cached(r, "public, max-age=600".into()) + } +} + +impl<'r, R: Responder<'r>> Responder<'r> for Cached { fn respond_to(self, req: &Request) -> response::Result<'r> { match self.0.respond_to(req) { Ok(mut res) => { - res.set_raw_header("Referrer-Policy", "same-origin"); - res.set_raw_header("X-Frame-Options", "SAMEORIGIN"); - res.set_raw_header("X-Content-Type-Options", "nosniff"); - res.set_raw_header("X-XSS-Protection", "1; mode=block"); - let csp = "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://*;"; - res.set_raw_header("Content-Security-Policy", csp); - + res.set_raw_header("Cache-Control", self.1); Ok(res) - }, - Err(_) => { - Err(Status::NotFound) } - } + e @ Err(_) => e, + } } } @@ -78,7 +93,6 @@ fn attachments(uuid: String, file: PathBuf) -> io::Result { NamedFile::open(Path::new(&CONFIG.attachments_folder).join(uuid).join(file)) } - #[get("/alive")] fn alive() -> Json { use crate::util::format_date; diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index d7881c28..a5974c36 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -74,7 +74,7 @@ impl Attachment { ) .map_res("Error deleting attachment")?; - crate::util::delete_file(&self.get_file_path()); + crate::util::delete_file(&self.get_file_path())?; Ok(()) } diff --git a/src/error.rs b/src/error.rs index 7f058b83..51afffa9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,7 @@ use diesel::result::Error as DieselError; use jsonwebtoken::errors::Error as JwtError; use serde_json::{Error as SerError, Value}; use u2f::u2ferror::U2fError as U2fErr; +use std::io::Error as IOError; // Error struct // Each variant has two elements, the first is an error of different types, used for logging purposes @@ -64,6 +65,7 @@ make_error! { U2fError(U2fErr, _): true, _api_error, SerdeError(SerError, _): true, _api_error, JWTError(JwtError, _): true, _api_error, + IoErrror(IOError, _): true, _api_error, //WsError(ws::Error, _): true, _api_error, } diff --git a/src/main.rs b/src/main.rs index c4fe9d2b..9c63620c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,12 +26,13 @@ fn init_rocket() -> Rocket { rocket::ignite() .mount("/", api::web_routes()) .mount("/api", api::core_routes()) - .mount("/admin", api::admin_routes()) + .mount("/admin", api::admin_routes()) .mount("/identity", api::identity_routes()) .mount("/icons", api::icons_routes()) .mount("/notifications", api::notifications_routes()) .manage(db::init_pool()) .manage(api::start_notification_server()) + .attach(util::AppHeaders()) } // Embed the migrations from the migrations folder into the application @@ -272,7 +273,6 @@ pub struct Config { signups_allowed: bool, invitations_allowed: bool, admin_token: Option, - server_admin_email: Option, password_iterations: i32, show_password_hint: bool, @@ -326,7 +326,6 @@ impl Config { local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false), signups_allowed: get_env_or("SIGNUPS_ALLOWED", true), admin_token: get_env("ADMIN_TOKEN"), - server_admin_email:None, // TODO: Delete this invitations_allowed: get_env_or("INVITATIONS_ALLOWED", true), password_iterations: get_env_or("PASSWORD_ITERATIONS", 100_000), show_password_hint: get_env_or("SHOW_PASSWORD_HINT", true), diff --git a/src/util.rs b/src/util.rs index 9a2a6073..2e01eb10 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,29 +1,57 @@ /// -/// File handling +/// Web Headers /// +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::{Request, Response}; -use std::path::Path; -use std::io::Read; +pub struct AppHeaders (); + +impl Fairing for AppHeaders { + fn info(&self) -> Info { + Info { + name: "Application Headers", + kind: Kind::Response, + } + } + + fn on_response(&self, _req: &Request, res: &mut Response) { + res.set_raw_header("Referrer-Policy", "same-origin"); + res.set_raw_header("X-Frame-Options", "SAMEORIGIN"); + res.set_raw_header("X-Content-Type-Options", "nosniff"); + res.set_raw_header("X-XSS-Protection", "1; mode=block"); + let csp = "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://*;"; + res.set_raw_header("Content-Security-Policy", csp); + + // Disable cache unless otherwise specified + if !res.headers().contains("cache-control") { + res.set_raw_header("Cache-Control", "no-cache, no-store, max-age=0"); + } + } +} + + +/// +/// File handling +/// use std::fs::{self, File}; +use std::io::{Read, Result as IOResult}; +use std::path::Path; pub fn file_exists(path: &str) -> bool { Path::new(path).exists() } -pub fn read_file(path: &str) -> Result, String> { - let mut file = File::open(Path::new(path)) - .map_err(|e| format!("Error opening file: {}", e))?; - +pub fn read_file(path: &str) -> IOResult> { let mut contents: Vec = Vec::new(); - - file.read_to_end(&mut contents) - .map_err(|e| format!("Error reading file: {}", e))?; + + let mut file = File::open(Path::new(path))?; + file.read_to_end(&mut contents)?; Ok(contents) } -pub fn delete_file(path: &str) -> bool { - let res = fs::remove_file(path).is_ok(); +pub fn delete_file(path: &str) -> IOResult<()> { + let res = fs::remove_file(path); if let Some(parent) = Path::new(path).parent() { // If the directory isn't empty, this returns an error, which we ignore @@ -34,7 +62,6 @@ pub fn delete_file(path: &str) -> bool { res } - const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"]; pub fn get_display_size(size: i32) -> String {