From 29a079521974027d12d6f504f37dcb42cc6a03d9 Mon Sep 17 00:00:00 2001 From: Jeremy Lin Date: Tue, 18 Feb 2020 21:27:00 -0800 Subject: [PATCH 1/4] Add backend support for alternate base dir (subdir/subpath) hosting To use this, include a path in the `DOMAIN` URL, e.g.: * `DOMAIN=https://example.com/custom-path` * `DOMAIN=https://example.com/multiple/levels/are/ok` --- src/api/admin.rs | 12 +++++++---- src/api/core/mod.rs | 2 +- src/api/web.rs | 14 +++++++++++-- src/auth.rs | 10 ++++----- src/config.rs | 21 +++++++++++++++++++ src/main.rs | 18 +++++++++------- src/static/templates/admin/base.hbs | 16 +++++++------- src/static/templates/admin/page.hbs | 18 ++++++++-------- .../templates/email/invite_accepted.hbs | 2 +- .../templates/email/invite_accepted.html.hbs | 2 +- .../templates/email/invite_confirmed.hbs | 2 +- .../templates/email/invite_confirmed.html.hbs | 2 +- .../templates/email/new_device_logged_in.hbs | 2 +- .../email/new_device_logged_in.html.hbs | 2 +- src/static/templates/email/pw_hint_some.hbs | 2 +- .../templates/email/pw_hint_some.html.hbs | 2 +- src/static/templates/email/welcome.hbs | 2 +- src/static/templates/email/welcome.html.hbs | 2 +- .../templates/email/welcome_must_verify.hbs | 2 +- .../email/welcome_must_verify.html.hbs | 2 +- src/util.rs | 13 ++++++++---- 21 files changed, 95 insertions(+), 53 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index c4fad117..b0c40b0c 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -52,6 +52,10 @@ const ADMIN_PATH: &str = "/admin"; const BASE_TEMPLATE: &str = "admin/base"; const VERSION: Option<&str> = option_env!("GIT_VERSION"); +fn admin_path() -> String { + format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) +} + #[get("/", rank = 2)] fn admin_login(flash: Option) -> ApiResult> { // If there is an error, show it @@ -76,7 +80,7 @@ fn post_admin_login(data: Form, mut cookies: Cookies, ip: ClientIp) - if !_validate_token(&data.token) { error!("Invalid admin token. IP: {}", ip.ip); Err(Flash::error( - Redirect::to(ADMIN_PATH), + Redirect::to(admin_path()), "Invalid admin token, please try again.", )) } else { @@ -85,14 +89,14 @@ fn post_admin_login(data: Form, mut cookies: Cookies, ip: ClientIp) - let jwt = encode_jwt(&claims); let cookie = Cookie::build(COOKIE_NAME, jwt) - .path(ADMIN_PATH) + .path(admin_path()) .max_age(chrono::Duration::minutes(20)) .same_site(SameSite::Strict) .http_only(true) .finish(); cookies.add(cookie); - Ok(Redirect::to(ADMIN_PATH)) + Ok(Redirect::to(admin_path())) } } @@ -167,7 +171,7 @@ fn invite_user(data: Json, _token: AdminToken, conn: DbConn) -> Empt #[get("/logout")] fn logout(mut cookies: Cookies) -> Result { cookies.remove(Cookie::named(COOKIE_NAME)); - Ok(Redirect::to(ADMIN_PATH)) + Ok(Redirect::to(admin_path())) } #[get("/users")] diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 55ed66dc..550733c0 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -172,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult { "BreachDate": "2019-08-18T00:00:00Z", "AddedDate": "2019-08-18T00:00:00Z", "Description": format!("Go to: https://haveibeenpwned.com/account/{account} for a manual check.

HaveIBeenPwned API key not set!
Go to https://haveibeenpwned.com/API/Key to purchase an API key from HaveIBeenPwned.

", account=username), - "LogoPath": "/bwrs_static/hibp.png", + "LogoPath": "bwrs_static/hibp.png", "PwnCount": 0, "DataClasses": [ "Error - No API key set!" diff --git a/src/api/web.rs b/src/api/web.rs index 408e38ee..7f47ae7c 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -37,7 +37,17 @@ fn app_id() -> Cached>> { { "version": { "major": 1, "minor": 0 }, "ids": [ - &CONFIG.domain(), + // Per : + // + // "In the Web case, the FacetID MUST be the Web Origin [RFC6454] + // of the web page triggering the FIDO operation, written as + // a URI with an empty path. Default ports are omitted and any + // path component is ignored." + // + // This leaves it unclear as to whether the path must be empty, + // or whether it can be non-empty and will be ignored. To be on + // the safe side, use a proper web origin (with empty path). + &CONFIG.domain_origin(), "ios:bundle-id:com.8bit.bitwarden", "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] }] @@ -75,6 +85,6 @@ fn static_files(filename: String) -> Result, Error> { "bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))), "md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))), "identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))), - _ => err!("Image not found"), + _ => err!(format!("Static file not found: {}", filename)), } } diff --git a/src/auth.rs b/src/auth.rs index 2820498c..cbcdb47a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -16,11 +16,11 @@ const JWT_ALGORITHM: Algorithm = Algorithm::RS256; lazy_static! { pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2); static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM); - pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain()); - pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain()); - pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain()); - pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain()); - pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain()); + pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain_origin()); + pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain_origin()); + pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain_origin()); + pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain_origin()); + pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain_origin()); static ref PRIVATE_RSA_KEY: Vec = match read_file(&CONFIG.private_rsa_key()) { Ok(key) => key, Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e), diff --git a/src/config.rs b/src/config.rs index 2c53a342..a8e440ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ use std::process::exit; use std::sync::RwLock; +use reqwest::Url; + use crate::error::Error; use crate::util::{get_env, get_env_bool}; @@ -240,6 +242,10 @@ make_config! { domain: String, true, def, "http://localhost".to_string(); /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; + /// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin) + domain_origin: String, false, auto, |c| extract_url_origin(&c.domain); + /// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path) + domain_path: String, false, auto, |c| extract_url_path(&c.domain); /// Enable web vault web_vault_enabled: bool, false, def, true; @@ -457,6 +463,21 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { Ok(()) } +/// Extracts an RFC 6454 web origin from a URL. +fn extract_url_origin(url: &str) -> String { + let url = Url::parse(url).expect("valid URL"); + + url.origin().ascii_serialization() +} + +/// Extracts the path from a URL. +/// All trailing '/' chars are trimmed, even if the path is a lone '/'. +fn extract_url_path(url: &str) -> String { + let url = Url::parse(url).expect("valid URL"); + + url.path().trim_end_matches('/').to_string() +} + impl Config { pub fn load() -> Result { // Loading from env and file diff --git a/src/main.rs b/src/main.rs index 7c9d0264..3c12063a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -255,18 +255,20 @@ mod migrations { } fn launch_rocket(extra_debug: bool) { - // Create Rocket object, this stores current log level and sets it's own + // Create Rocket object, this stores current log level and sets its own let rocket = rocket::ignite(); - // If addding more base paths here, consider also adding them to + let basepath = &CONFIG.domain_path(); + + // If adding more paths here, consider also adding them to // crate::utils::LOGGED_ROUTES to make sure they appear in the log let rocket = rocket - .mount("/", api::web_routes()) - .mount("/api", api::core_routes()) - .mount("/admin", api::admin_routes()) - .mount("/identity", api::identity_routes()) - .mount("/icons", api::icons_routes()) - .mount("/notifications", api::notifications_routes()) + .mount(&[basepath, "/"].concat(), api::web_routes()) + .mount(&[basepath, "/api"].concat(), api::core_routes()) + .mount(&[basepath, "/admin"].concat(), api::admin_routes()) + .mount(&[basepath, "/identity"].concat(), api::identity_routes()) + .mount(&[basepath, "/icons"].concat(), api::icons_routes()) + .mount(&[basepath, "/notifications"].concat(), api::notifications_routes()) .manage(db::init_pool()) .manage(api::start_notification_server()) .attach(util::AppHeaders()) diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs index f61ae6ae..9222a19c 100644 --- a/src/static/templates/admin/base.hbs +++ b/src/static/templates/admin/base.hbs @@ -6,10 +6,10 @@ Bitwarden_rs Admin Panel - - - - + + + +