From 538dc00234c9e6e27026b30ce13f19a14369aa95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Garc=C3=ADa?=
 <dani-garcia@users.noreply.github.com>
Date: Tue, 12 Jun 2018 21:09:42 +0200
Subject: [PATCH] Improved configuration and documented options. Implemented
 option to disable web vault and to disable the use of bitwarden's official
 icon servers

---
 .env             |  31 ++++++++++++---
 src/api/icons.rs | 100 +++++++++++++++++++++++++++++------------------
 src/api/web.rs   |  10 +++--
 src/main.rs      |  18 ++++++---
 4 files changed, 108 insertions(+), 51 deletions(-)

diff --git a/.env b/.env
index 4b481b62..152f4cec 100644
--- a/.env
+++ b/.env
@@ -1,13 +1,34 @@
+## Bitwarden_RS Configuration File
+## Uncomment any of the following lines to change the defaults
+
+## Main data folder
+# DATA_FOLDER=data
+
+## Individual folders, these override %DATA_FOLDER%
 # DATABASE_URL=data/db.sqlite3
-# PRIVATE_RSA_KEY=data/private_rsa_key.der
-# PUBLIC_RSA_KEY=data/public_rsa_key.der
+# RSA_KEY_FILENAME=data/rsa_key
 # ICON_CACHE_FOLDER=data/icon_cache
 # ATTACHMENTS_FOLDER=data/attachments
 
-# true for yes, anything else for no
-SIGNUPS_ALLOWED=true
+## Web vault settings
+# WEB_VAULT_FOLDER=web-vault/
+# WEB_VAULT_ENABLED=true
 
-# ROCKET_ENV=production
+## Controls if new users can register
+# SIGNUPS_ALLOWED=true
+
+## Use a local favicon extractor
+## Set to false to use bitwarden's official icon servers
+## Set to true to use the local version, which is not as smart,
+##   but it doesn't send the cipher domains to bitwarden's servers
+# LOCAL_ICON_EXTRACTOR=false
+
+## Controls the PBBKDF password iterations to apply on the server
+## The change only applies when the password is changed
+# PASSWORD_ITERATIONS=100000
+
+## Rocket specific settings, check Rocket documentation to learn more
+# ROCKET_ENV=staging
 # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
 # ROCKET_PORT=8000
 # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
diff --git a/src/api/icons.rs b/src/api/icons.rs
index 2ca29faf..6783d32b 100644
--- a/src/api/icons.rs
+++ b/src/api/icons.rs
@@ -1,4 +1,3 @@
-use std::io;
 use std::io::prelude::*;
 use std::fs::{create_dir_all, File};
 
@@ -23,24 +22,56 @@ fn icon(domain: String) -> Content<Vec<u8>> {
         return Content(icon_type, get_fallback_icon());
     }
 
-    let url = format!("https://icons.bitwarden.com/{}/icon.png", domain);
-
-    // Get the icon, or fallback in case of error
-    let icon = match get_icon_cached(&domain, &url) {
-        Ok(icon) => icon,
-        Err(_) => return Content(icon_type, get_fallback_icon())
-    };
+    let icon = get_icon(&domain);
 
     Content(icon_type, icon)
 }
 
-fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
+fn get_icon (domain: &str) -> Vec<u8> {
+    let path = format!("{}/{}.png", CONFIG.icon_cache_folder, domain);
+
+    if let Some(icon) = get_cached_icon(&path) {
+        return icon;
+    }
+
+    let url = get_icon_url(&domain);
+
+    // Get the icon, or fallback in case of error
+    match download_icon(&url) {
+        Ok(icon) => {
+            save_icon(&path, &icon);
+            icon
+        },
+        Err(_) => get_fallback_icon()
+    }
+}
+
+fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
+    // Try to read the cached icon, and return it if it exists
+    if let Ok(mut f) = File::open(path) {
+        let mut buffer = Vec::new();
+
+        if f.read_to_end(&mut buffer).is_ok() {
+            return Some(buffer);
+        }
+    }
+
+    None
+}
+
+fn get_icon_url(domain: &str) -> String {
+    if CONFIG.local_icon_extractor {
+        format!("http://{}/favicon.ico", domain)
+    } else {
+        format!("https://icons.bitwarden.com/{}/icon.png", domain)
+    }
+}
+
+fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
+    println!("Downloading icon for {}...", url);
     let mut res = reqwest::get(url)?;
 
-    res = match res.error_for_status() {
-        Err(e) => return Err(e),
-        Ok(res) => res
-    };
+    res = res.error_for_status()?;
 
     let mut buffer: Vec<u8> = vec![];
     res.copy_to(&mut buffer)?;
@@ -48,35 +79,28 @@ fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
     Ok(buffer)
 }
 
-fn get_icon_cached(key: &str, url: &str) -> io::Result<Vec<u8>> {
-    create_dir_all(&CONFIG.icon_cache_folder)?;
-    let path = &format!("{}/{}.png", CONFIG.icon_cache_folder, key);
+fn save_icon(path: &str, icon: &[u8]) {
+    create_dir_all(&CONFIG.icon_cache_folder).expect("Error creating icon cache");
 
-    // Try to read the cached icon, and return it if it exists
-    if let Ok(mut f) = File::open(path) {
-        let mut buffer = Vec::new();
-
-        if f.read_to_end(&mut buffer).is_ok() {
-            return Ok(buffer);
-        }
-        /* If error reading file continue */
-    }
-
-    println!("Downloading icon for {}...", key);
-    let icon = match get_icon(url) {
-        Ok(icon) => icon,
-        Err(_) => return Err(io::Error::new(io::ErrorKind::NotFound, ""))
-    };
-
-    // Save the currently downloaded icon
     if let Ok(mut f) = File::create(path) {
-        f.write_all(&icon).expect("Error writing icon file");
+        f.write_all(icon).expect("Error writing icon file");
     };
-
-    Ok(icon)
 }
 
+const FALLBACK_ICON_URL: &str = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
+
 fn get_fallback_icon() -> Vec<u8> {
-    let fallback_icon = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
-    get_icon_cached("default", fallback_icon).unwrap()
+    let path = format!("{}/default.png", CONFIG.icon_cache_folder);
+    
+    if let Some(icon) = get_cached_icon(&path) {
+        return icon;
+    }
+
+    match download_icon(FALLBACK_ICON_URL) {
+        Ok(icon) => {
+            save_icon(&path, &icon);
+            icon
+        },
+        Err(_) => vec![]
+    }
 }
diff --git a/src/api/web.rs b/src/api/web.rs
index 8e39b3a9..d65452dd 100644
--- a/src/api/web.rs
+++ b/src/api/web.rs
@@ -8,19 +8,23 @@ use rocket_contrib::Json;
 use CONFIG;
 
 pub fn routes() -> Vec<Route> {
-    routes![index, files, attachments, alive]
+    if CONFIG.web_vault_enabled {
+        routes![web_index, web_files, attachments, alive]
+    } else {
+        routes![attachments, alive]
+    }
 }
 
 // TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
 #[get("/")]
-fn index() -> io::Result<NamedFile> {
+fn web_index() -> io::Result<NamedFile> {
     NamedFile::open(
         Path::new(&CONFIG.web_vault_folder)
             .join("index.html"))
 }
 
 #[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
-fn files(p: PathBuf) -> io::Result<NamedFile> {
+fn web_files(p: PathBuf) -> io::Result<NamedFile> {
     NamedFile::open(
         Path::new(&CONFIG.web_vault_folder)
             .join(p))
diff --git a/src/main.rs b/src/main.rs
index 3e2dd3dc..99941732 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -127,6 +127,10 @@ fn check_rsa_keys() {
 }
 
 fn check_web_vault() {
+    if !CONFIG.web_vault_enabled {
+        return;
+    }
+
     let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
 
     if !index_path.exists() {
@@ -151,7 +155,9 @@ pub struct Config {
     public_rsa_key: String,
 
     web_vault_folder: String,
+    web_vault_enabled: bool,
 
+    local_icon_extractor: bool,
     signups_allowed: bool,
     password_iterations: i32,
 }
@@ -161,20 +167,22 @@ impl Config {
         dotenv::dotenv().ok();
 
         let df = env::var("DATA_FOLDER").unwrap_or("data".into());
-        let key = env::var("RSA_KEY_NAME").unwrap_or("rsa_key".into());
+        let key = env::var("RSA_KEY_FILENAME").unwrap_or(format!("{}/{}", &df, "rsa_key"));
 
         Config {
             database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
             icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
             attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
 
-            private_rsa_key: format!("{}/{}.der", &df, &key),
-            private_rsa_key_pem: format!("{}/{}.pem", &df, &key),
-            public_rsa_key: format!("{}/{}.pub.der", &df, &key),
+            private_rsa_key: format!("{}.der", &key),
+            private_rsa_key_pem: format!("{}.pem", &key),
+            public_rsa_key: format!("{}.pub.der", &key),
 
             web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
+            web_vault_enabled: util::parse_option_string(env::var("WEB_VAULT_ENABLED").ok()).unwrap_or(true),
 
-            signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(false),
+            local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
+            signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
             password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
         }
     }