Browse Source

Merge pull request #868 from jjlin/alt-base

Add backend support for alternate base dir (subdir/subpath) hosting
Daniel García 5 years ago
parent
commit
8a5450e830
34 changed files with 125 additions and 94 deletions
  1. 2 3
      docker/Dockerfile.j2
  2. 2 3
      docker/aarch64/mysql/Dockerfile
  3. 2 3
      docker/aarch64/sqlite/Dockerfile
  4. 2 3
      docker/amd64/mysql/Dockerfile
  5. 2 3
      docker/amd64/mysql/Dockerfile.alpine
  6. 2 3
      docker/amd64/postgresql/Dockerfile
  7. 2 3
      docker/amd64/postgresql/Dockerfile.alpine
  8. 2 3
      docker/amd64/sqlite/Dockerfile
  9. 2 3
      docker/amd64/sqlite/Dockerfile.alpine
  10. 2 3
      docker/armv6/mysql/Dockerfile
  11. 2 3
      docker/armv6/sqlite/Dockerfile
  12. 2 3
      docker/armv7/mysql/Dockerfile
  13. 2 3
      docker/armv7/sqlite/Dockerfile
  14. 11 5
      src/api/admin.rs
  15. 1 1
      src/api/core/mod.rs
  16. 12 2
      src/api/web.rs
  17. 5 5
      src/auth.rs
  18. 21 0
      src/config.rs
  19. 10 8
      src/main.rs
  20. 8 8
      src/static/templates/admin/base.hbs
  21. 10 10
      src/static/templates/admin/page.hbs
  22. 1 1
      src/static/templates/email/invite_accepted.hbs
  23. 1 1
      src/static/templates/email/invite_accepted.html.hbs
  24. 1 1
      src/static/templates/email/invite_confirmed.hbs
  25. 1 1
      src/static/templates/email/invite_confirmed.html.hbs
  26. 1 1
      src/static/templates/email/new_device_logged_in.hbs
  27. 1 1
      src/static/templates/email/new_device_logged_in.html.hbs
  28. 1 1
      src/static/templates/email/pw_hint_some.hbs
  29. 1 1
      src/static/templates/email/pw_hint_some.html.hbs
  30. 1 1
      src/static/templates/email/welcome.hbs
  31. 1 1
      src/static/templates/email/welcome.html.hbs
  32. 1 1
      src/static/templates/email/welcome_must_verify.hbs
  33. 1 1
      src/static/templates/email/welcome_must_verify.html.hbs
  34. 9 4
      src/util.rs

+ 2 - 3
docker/Dockerfile.j2

@@ -31,7 +31,7 @@
 {% endif %}
 FROM {{ vault_stage_base_image }} as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
@@ -42,8 +42,7 @@ RUN apk add --no-cache --upgrade curl tar
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 {% endif %}
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 {% if "alpine" in vault_stage_base_image %}
 SHELL ["/bin/ash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]

+ 2 - 3
docker/aarch64/mysql/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/aarch64/sqlite/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/mysql/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/mysql/Dockerfile.alpine

@@ -7,14 +7,13 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM alpine:3.11 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 RUN apk add --no-cache --upgrade curl tar
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/ash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/postgresql/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/postgresql/Dockerfile.alpine

@@ -7,14 +7,13 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM alpine:3.11 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 RUN apk add --no-cache --upgrade curl tar
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/ash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/sqlite/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/amd64/sqlite/Dockerfile.alpine

@@ -7,14 +7,13 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM alpine:3.11 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 RUN apk add --no-cache --upgrade curl tar
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/ash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/armv6/mysql/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/armv6/sqlite/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/armv7/mysql/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 2 - 3
docker/armv7/sqlite/Dockerfile

@@ -7,15 +7,14 @@
 ####################### VAULT BUILD IMAGE  #######################
 FROM rust:1.40 as vault
 
-ENV VAULT_VERSION "v2.12.0c"
+ENV VAULT_VERSION "v2.12.0d"
 
 ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
 
 # Build time options to avoid dpkg warnings and help with reproducible builds.
 ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
 
-RUN mkdir /web-vault
-WORKDIR /web-vault
+WORKDIR /
 
 SHELL ["/bin/bash", "-o", "nounset", "-o", "pipefail", "-o", "errexit", "-c"]
 

+ 11 - 5
src/api/admin.rs

@@ -52,11 +52,15 @@ 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<FlashMessage>) -> ApiResult<Html<String>> {
     // If there is an error, show it
     let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
-    let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg});
+    let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg, "urlpath": CONFIG.domain_path()});
 
     // Return the page
     let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
@@ -76,7 +80,7 @@ fn post_admin_login(data: Form<LoginForm>, 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<LoginForm>, 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()))
     }
 }
 
@@ -111,6 +115,7 @@ struct AdminTemplateData {
     config: Value,
     can_backup: bool,
     logged_in: bool,
+    urlpath: String,
 }
 
 impl AdminTemplateData {
@@ -122,6 +127,7 @@ impl AdminTemplateData {
             config: CONFIG.prepare_json(),
             can_backup: *CAN_BACKUP,
             logged_in: true,
+            urlpath: CONFIG.domain_path(),
         }
     }
 
@@ -167,7 +173,7 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
 #[get("/logout")]
 fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
     cookies.remove(Cookie::named(COOKIE_NAME));
-    Ok(Redirect::to(ADMIN_PATH))
+    Ok(Redirect::to(admin_path()))
 }
 
 #[get("/users")]

+ 1 - 1
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: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
-            "LogoPath": "/bwrs_static/hibp.png",
+            "LogoPath": "bwrs_static/hibp.png",
             "PwnCount": 0,
             "DataClasses": [
                 "Error - No API key set!"

+ 12 - 2
src/api/web.rs

@@ -37,7 +37,17 @@ fn app_id() -> Cached<Content<Json<Value>>> {
             {
             "version": { "major": 1, "minor": 0 },
             "ids": [
-                &CONFIG.domain(),
+                // Per <https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-the-facetid-of-a-calling-application>:
+                //
+                // "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<Content<&'static [u8]>, 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)),
     }
 }

+ 5 - 5
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<u8> = match read_file(&CONFIG.private_rsa_key()) {
         Ok(key) => key,
         Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),

+ 21 - 0
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<Self, Error> {
         // Loading from env and file

+ 10 - 8
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())

+ 8 - 8
src/static/templates/admin/base.hbs

@@ -6,10 +6,10 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <title>Bitwarden_rs Admin Panel</title>
 
-    <link rel="stylesheet" href="/bwrs_static/bootstrap.css" />
-    <script src="/bwrs_static/bootstrap-native-v4.js"></script>
-    <script src="/bwrs_static/md5.js"></script>
-    <script src="/bwrs_static/identicon.js"></script>
+    <link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
+    <script src="{{urlpath}}/bwrs_static/bootstrap-native-v4.js"></script>
+    <script src="{{urlpath}}/bwrs_static/md5.js"></script>
+    <script src="{{urlpath}}/bwrs_static/identicon.js"></script>
     <style>
         body {
             padding-top: 70px;
@@ -38,10 +38,10 @@
         <div class="navbar-collapse">
             <ul class="navbar-nav">
                 <li class="nav-item active">
-                    <a class="nav-link" href="/admin">Admin Panel</a>
+                    <a class="nav-link" href="{{urlpath}}/admin">Admin Panel</a>
                 </li>
                 <li class="nav-item">
-                    <a class="nav-link" href="/">Vault</a>
+                    <a class="nav-link" href="{{urlpath}}/">Vault</a>
                 </li>
             </ul>
         </div>
@@ -55,7 +55,7 @@
 
             {{#if logged_in}}
             <li class="nav-item">
-                <a class="nav-link" href="/admin/logout">Log Out</a>
+                <a class="nav-link" href="{{urlpath}}/admin/logout">Log Out</a>
             </li>
             {{/if}}
         </ul>
@@ -64,4 +64,4 @@
     {{> (page_content) }}
 </body>
 
-</html>
+</html>

+ 10 - 10
src/static/templates/admin/page.hbs

@@ -19,7 +19,7 @@
                             <span class="d-block">{{Email}}</span>
                         </div>
                         <div class="col">
-                            <strong> Organizations:</strong>
+                            <strong> Organizations: </strong>
                             <span class="d-block">
                                 {{#each Organizations}}
                                 <span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span>
@@ -225,7 +225,7 @@
         var input_mail = prompt("To delete user '" + mail + "', please type the email below")
         if (input_mail != null) {
             if (input_mail == mail) {
-                _post("/admin/users/" + id + "/delete",
+                _post("{{urlpath}}/admin/users/" + id + "/delete",
                     "User deleted correctly",
                     "Error deleting user");
             } else {
@@ -235,19 +235,19 @@
         return false;
     }
     function remove2fa(id) {
-        _post("/admin/users/" + id + "/remove-2fa",
+        _post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
             "2FA removed correctly",
             "Error removing 2FA");
         return false;
     }
     function deauthUser(id) {
-        _post("/admin/users/" + id + "/deauth",
+        _post("{{urlpath}}/admin/users/" + id + "/deauth",
             "Sessions deauthorized correctly",
             "Error deauthorizing sessions");
         return false;
     }
     function updateRevisions() {
-        _post("/admin/users/update_revision",
+        _post("{{urlpath}}/admin/users/update_revision",
             "Success, clients will sync next time they connect",
             "Error forcing clients to sync");
         return false;
@@ -256,7 +256,7 @@
         inv = document.getElementById("email-invite");
         data = JSON.stringify({ "email": inv.value });
         inv.value = "";
-        _post("/admin/invite/", "User invited correctly",
+        _post("{{urlpath}}/admin/invite/", "User invited correctly",
             "Error inviting user", data);
         return false;
     }
@@ -278,7 +278,7 @@
     }
     function saveConfig() {
         data = JSON.stringify(getFormData());
-        _post("/admin/config/", "Config saved correctly",
+        _post("{{urlpath}}/admin/config/", "Config saved correctly",
             "Error saving config", data);
         return false;
     }
@@ -286,7 +286,7 @@
         var input = prompt("This will remove all user configurations, and restore the defaults and the " +
             "values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:");
         if (input === "DELETE") {
-            _post("/admin/config/delete",
+            _post("{{urlpath}}/admin/config/delete",
                 "Config deleted correctly",
                 "Error deleting config");
         } else {
@@ -296,7 +296,7 @@
         return false;
     }
     function backupDatabase() {
-        _post("/admin/config/backup_db",
+        _post("{{urlpath}}/admin/config/backup_db",
             "Backup created successfully",
             "Error creating backup");
         return false;
@@ -336,4 +336,4 @@
     // {{#each config}} {{#if grouptoggle}}
     masterCheck("input_{{grouptoggle}}", "#g_{{group}} input");
     // {{/if}} {{/each}}
-</script>
+</script>

+ 1 - 1
src/static/templates/email/invite_accepted.hbs

@@ -3,6 +3,6 @@ Invitation accepted
 <html>
 <p>
     Your invitation for <b>{{email}}</b> to join <b>{{org_name}}</b> was accepted.
-    Please <a href="{{url}}">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
+    Please <a href="{{url}}/">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
 </p>
 </html>

+ 1 - 1
src/static/templates/email/invite_accepted.html.hbs

@@ -101,7 +101,7 @@ Invitation accepted
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
-                                          Please <a href="{{url}}">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
+                                          Please <a href="{{url}}/">log in</a> to the bitwarden_rs server and confirm them from the organization management page.
                                        </td>
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

+ 1 - 1
src/static/templates/email/invite_confirmed.hbs

@@ -3,6 +3,6 @@ Invitation to {{org_name}} confirmed
 <html>
 <p>
     Your invitation to join <b>{{org_name}}</b> was confirmed.
-    It will now appear under the Organizations the next time you <a href="{{url}}">log in</a> to the web vault.
+    It will now appear under the Organizations the next time you <a href="{{url}}/">log in</a> to the web vault.
 </p>
 </html>

+ 1 - 1
src/static/templates/email/invite_confirmed.html.hbs

@@ -102,7 +102,7 @@ Invitation to {{org_name}} confirmed
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
                                           Any collections and logins being shared with you by this organization will now appear in your Bitwarden vault. <br>
-                                          <a href="{{url}}">Log in</a>
+                                          <a href="{{url}}/">Log in</a>
                                        </td>
                                     </tr>
                                  </table>

+ 1 - 1
src/static/templates/email/new_device_logged_in.hbs

@@ -9,6 +9,6 @@ New Device Logged In From {{device}}
    Device Type: {{device}}
 
    You can deauthorize all devices that have access to your account from the
-    <a href="{{url}}">web vault</a> under Settings > My Account > Deauthorize Sessions.
+    <a href="{{url}}/">web vault</a> under Settings > My Account > Deauthorize Sessions.
 </p>
 </html>

+ 1 - 1
src/static/templates/email/new_device_logged_in.html.hbs

@@ -116,7 +116,7 @@ New Device Logged In From {{device}}
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
-                                           You can deauthorize all devices that have access to your account from the <a href="{{url}}">web vault</a> under Settings > My Account > Deauthorize Sessions.
+                                           You can deauthorize all devices that have access to your account from the <a href="{{url}}/">web vault</a> under Settings > My Account > Deauthorize Sessions.
                                        </td>
                                     </tr>
                                  </table>

+ 1 - 1
src/static/templates/email/pw_hint_some.hbs

@@ -3,7 +3,7 @@ Your master password hint
 You (or someone) recently requested your master password hint.
 
 Your hint is: "{{hint}}"
-Log in: <a href="{{url}}">Web Vault</a>
+Log in: <a href="{{url}}/">Web Vault</a>
 
 If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to <a href="{{url}}/#/recover-delete">delete the account</a> so that you can register again and start over. All data associated with your account will be deleted.
 

+ 1 - 1
src/static/templates/email/pw_hint_some.html.hbs

@@ -102,7 +102,7 @@ Your master password hint
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
                                           Your hint is: "{{hint}}"<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
-                                          Log in: <a href="{{url}}">Web Vault</a>
+                                          Log in: <a href="{{url}}/">Web Vault</a>
                                        </td>
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

+ 1 - 1
src/static/templates/email/welcome.hbs

@@ -2,7 +2,7 @@ Welcome
 <!---------------->
 <html>
 <p>
-Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
+Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. You may now log in with your new account.
 </p>
 <p>If you did not request to create an account, you can safely ignore this email.</p>
 </html>

+ 1 - 1
src/static/templates/email/welcome.html.hbs

@@ -96,7 +96,7 @@ Welcome
                                  <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
-                                          Thank you for creating an account at <a href="{{url}}">{{url}}</a>. You may now log in with your new account.
+                                          Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. You may now log in with your new account.
                                        </td>
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

+ 1 - 1
src/static/templates/email/welcome_must_verify.hbs

@@ -2,7 +2,7 @@ Welcome
 <!---------------->
 <html>
 <p>
-Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
+Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
 <br>
 <br>
 <a href="{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}">

+ 1 - 1
src/static/templates/email/welcome_must_verify.html.hbs

@@ -96,7 +96,7 @@ Welcome
                                  <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
                                        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
-                                          Thank you for creating an account at <a href="{{url}}">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
+                                          Thank you for creating an account at <a href="{{url}}/">{{url}}</a>. Before you can login with your new account, you must verify this email address by clicking the link below.
                                        </td>
                                     </tr>
                                     <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

+ 9 - 4
src/util.rs

@@ -109,7 +109,7 @@ impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
     }
 }
 
-// Log all the routes from the main base paths list, and the attachments endoint
+// Log all the routes from the main paths list, and the attachments endpoint
 // Effectively ignores, any static file route, and the alive endpoint
 const LOGGED_ROUTES: [&str; 6] = [
     "/api",
@@ -157,7 +157,10 @@ impl Fairing for BetterLogging {
         }
         let uri = request.uri();
         let uri_path = uri.path();
-        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_path.starts_with(r)) {
+        // FIXME: trim_start_matches() could result in over-trimming in pathological cases;
+        // strip_prefix() would be a better option once it's stable.
+        let uri_subpath = uri_path.trim_start_matches(&CONFIG.domain_path());
+        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
             match uri.query() {
                 Some(q) => info!(target: "request", "{} {}?{}", method, uri_path, &q[..q.len().min(30)]),
                 None => info!(target: "request", "{} {}", method, uri_path),
@@ -169,8 +172,10 @@ impl Fairing for BetterLogging {
         if !self.0 && request.method() == Method::Options {
             return;
         }
-        let uri_path = request.uri().path();
-        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_path.starts_with(r)) {
+        // FIXME: trim_start_matches() could result in over-trimming in pathological cases;
+        // strip_prefix() would be a better option once it's stable.
+        let uri_subpath = request.uri().path().trim_start_matches(&CONFIG.domain_path());
+        if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
             let status = response.status();
             if let Some(ref route) = request.route() {
                 info!(target: "response", "{} => {} {}", route, status.code, status.reason)