forked from trashmodern/vaultwarden
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 349cb33fbd | |||
| d7542b6818 | |||
| 7976d39d9d | |||
| 5ee9676941 | |||
| 4b40cda910 | |||
| 4689ed7b30 | |||
| 084bc2aee3 | |||
| 6d7e15b2fd | |||
| 61515160a7 | |||
| a25bfdd16d | |||
| e93538cea9 | |||
| b4244b28b6 | |||
| 43f9038325 | |||
| 27872f476e | |||
| 339044f8aa | |||
| 0718a090e1 | |||
| 9e1f030a80 | |||
| 04922f6aa0 | |||
| 7d2bc9e162 | |||
| c6c00729e3 | |||
| 10756b0920 | |||
| 1eb1502a07 | |||
| 30e72a96a9 | |||
| 2646db78a4 | |||
| f5358b13f5 | |||
| d156170971 | |||
| d9bfe847db | |||
| 473f8b8e31 | |||
| aeb4b4c8a5 | |||
| 980a3e45db | |||
| 5794969f5b | |||
| 8b5b06c3d1 | |||
| b50c27b619 | |||
| 5ee04e31e5 | |||
| bf6ae91a6d | |||
| 828e3a5795 | |||
| 7b5bcd45f8 | |||
| 72de16fb86 | |||
| 0b903fc5f4 | |||
| 4df686f49e | |||
| d7eeaaf249 | |||
| a744b9437a | |||
| 6027b969f5 | |||
| 93805a5d7b | |||
| 71da961ecd | |||
| dd421809e5 | |||
| 274ea9a4f2 | |||
| 8743d18aca | |||
| d3773a433a | |||
| 0f0a87becf | |||
| 4b57bb8eeb | |||
| 3b27dbb0aa | |||
| ff2fbd322e | |||
| 9636f33fdb | |||
| bbe2a1b264 | |||
| 79fdfd6524 | |||
| d086a99e5b | |||
| 22b0b95209 | |||
| 28d1588e73 | |||
| f3b1a5ff3e | |||
| 330e90a6ac |
@@ -9,10 +9,6 @@ data
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
|
||||
+25
-1
@@ -43,12 +43,35 @@
|
||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||
# LOG_FILE=/path/to/log
|
||||
|
||||
## Enable WAL for the DB
|
||||
## Set to false to avoid enabling WAL during startup.
|
||||
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||
## this setting only prevents bitwarden_rs from automatically enabling it on start.
|
||||
## Please read project wiki page about this setting first before changing the value as it can
|
||||
## cause performance degradation or might render the service unable to start.
|
||||
# ENABLE_DB_WAL=true
|
||||
|
||||
## Disable icon downloading
|
||||
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
||||
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||
## otherwise it will delete them and they won't be downloaded again.
|
||||
# DISABLE_ICON_DOWNLOAD=false
|
||||
|
||||
## Icon download timeout
|
||||
## Configure the timeout value when downloading the favicons.
|
||||
## The default is 10 seconds, but this could be to low on slower network connections
|
||||
# ICON_DOWNLOAD_TIMEOUT=10
|
||||
|
||||
## Icon blacklist Regex
|
||||
## Any domains or IPs that match this regex won't be fetched by the icon service.
|
||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
|
||||
|
||||
## Disable 2FA remember
|
||||
## Enabling this would force the users to use a second factor to login every time.
|
||||
## Note that the checkbox would still be present, but ignored.
|
||||
# DISABLE_2FA_REMEMBER=false
|
||||
|
||||
## Controls if new users can register
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
@@ -56,6 +79,7 @@
|
||||
## One option is to use 'openssl rand -base64 48'
|
||||
## If not set, the admin panel is disabled
|
||||
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
|
||||
# DISABLE_ADMIN_TOKEN=false
|
||||
|
||||
## Invitations org admins to invite users, even when signups are disabled
|
||||
# INVITATIONS_ALLOWED=true
|
||||
@@ -97,4 +121,4 @@
|
||||
# SMTP_PORT=587
|
||||
# SMTP_SSL=true
|
||||
# SMTP_USERNAME=username
|
||||
# SMTP_PASSWORD=password
|
||||
# SMTP_PASSWORD=password
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
# Copied from Rocket's .travis.yml
|
||||
dist: xenial
|
||||
|
||||
language: rust
|
||||
sudo: required # so we get a VM with higher specs
|
||||
dist: trusty # so we get a VM with higher specs
|
||||
rust: nightly
|
||||
cache: cargo
|
||||
rust:
|
||||
- nightly
|
||||
script:
|
||||
- cargo build --verbose --all-features
|
||||
|
||||
# Nothing to install
|
||||
install: true
|
||||
script: cargo build --all-features
|
||||
|
||||
Generated
+397
-507
File diff suppressed because it is too large
Load Diff
+11
-16
@@ -19,24 +19,24 @@ rocket = { version = "0.4.0", features = ["tls"], default-features = false }
|
||||
rocket_contrib = "0.4.0"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.9.9"
|
||||
reqwest = "0.9.12"
|
||||
|
||||
# multipart/form-data support
|
||||
multipart = "0.16.1"
|
||||
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
||||
|
||||
# WebSockets library
|
||||
ws = "0.7.9"
|
||||
ws = "0.8.0"
|
||||
|
||||
# MessagePack library
|
||||
rmpv = "0.4.0"
|
||||
|
||||
# Concurrent hashmap implementation
|
||||
chashmap = "2.2.0"
|
||||
chashmap = "2.2.2"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.85"
|
||||
serde_derive = "1.0.85"
|
||||
serde_json = "1.0.37"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
serde_json = "1.0.39"
|
||||
|
||||
# Logging
|
||||
log = "0.4.6"
|
||||
@@ -44,7 +44,7 @@ fern = "0.5.7"
|
||||
syslog = { version = "4.0.1", optional = true }
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "1.4.1", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
||||
|
||||
# Bundled SQLite
|
||||
@@ -78,10 +78,10 @@ yubico = { version = "0.5.1", features = ["online"], default-features = false }
|
||||
dotenv = { version = "0.13.0", default-features = false }
|
||||
|
||||
# Lazy static macro
|
||||
lazy_static = { version = "1.2.0", features = ["nightly"] }
|
||||
lazy_static = "1.3.0"
|
||||
|
||||
# More derives
|
||||
derive_more = "0.13.0"
|
||||
derive_more = "0.14.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.6"
|
||||
@@ -97,13 +97,8 @@ handlebars = "1.1.0"
|
||||
|
||||
# For favicon extraction from main website
|
||||
soup = "0.3.0"
|
||||
regex = "1.1.0"
|
||||
regex = "1.1.2"
|
||||
|
||||
[patch.crates-io]
|
||||
# Add support for Timestamp type
|
||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||
|
||||
# Use new native_tls version 0.2
|
||||
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
||||
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.8.0d"
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.8.0d"
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.8.0d"
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Using multistage build:
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
curl \
|
||||
tar
|
||||
|
||||
RUN mkdir /web-vault
|
||||
WORKDIR /web-vault
|
||||
|
||||
RUN curl -L $URL | tar xz
|
||||
RUN ls
|
||||
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rust as build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
gcc-arm-linux-gnueabi \
|
||||
&& mkdir -p ~/.cargo \
|
||||
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
|
||||
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config
|
||||
|
||||
ENV CARGO_HOME "/root/.cargo"
|
||||
ENV USER "root"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Prepare openssl armel libs
|
||||
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
|
||||
/etc/apt/sources.list.d/deb-src.list \
|
||||
&& dpkg --add-architecture armel \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
libssl-dev:armel \
|
||||
libc6-dev:armel
|
||||
|
||||
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
|
||||
ENV CROSS_COMPILE="1"
|
||||
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
|
||||
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
|
||||
|
||||
# Copies the complete project
|
||||
# To avoid copying unneeded files, use .dockerignore
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN rustup target add arm-unknown-linux-gnueabi
|
||||
RUN cargo build --release --target=arm-unknown-linux-gnueabi -v
|
||||
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM balenalib/rpi-debian:stretch
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
ENV ROCKET_WORKERS=10
|
||||
|
||||
RUN [ "cross-build-start" ]
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& ln -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
|
||||
# Copies the files from the context (Rocket.toml file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
CMD ./bitwarden_rs
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.8.0d"
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
pool:
|
||||
vmImage: 'Ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
ls -la
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
|
||||
echo "##vso[task.prependpath]$HOME/.cargo/bin"
|
||||
displayName: 'Install Rust'
|
||||
|
||||
- script: |
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
displayName: Query rust and cargo versions
|
||||
|
||||
- script : cargo build --all-features
|
||||
displayName: 'Build project'
|
||||
@@ -6,6 +6,10 @@ fn main() {
|
||||
|
||||
fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
||||
let out = Command::new(args[0]).args(&args[1..]).output()?;
|
||||
if !out.status.success() {
|
||||
use std::io::{Error, ErrorKind};
|
||||
return Err(Error::new(ErrorKind::Other, "Command not successful"));
|
||||
}
|
||||
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
|
||||
}
|
||||
|
||||
@@ -13,8 +17,10 @@ fn run(args: &[&str]) -> Result<String, std::io::Error> {
|
||||
fn read_git_info() -> Result<(), std::io::Error> {
|
||||
// The exact tag for the current commit, can be empty when
|
||||
// the current commit doesn't have an associated tag
|
||||
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"])?;
|
||||
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact_tag);
|
||||
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
|
||||
if let Some(ref exact) = exact_tag {
|
||||
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact);
|
||||
}
|
||||
|
||||
// The last available tag, equal to exact_tag when
|
||||
// the current commit is tagged
|
||||
@@ -27,13 +33,25 @@ fn read_git_info() -> Result<(), std::io::Error> {
|
||||
|
||||
// The current git commit hash
|
||||
let rev = run(&["git", "rev-parse", "HEAD"])?;
|
||||
let rev_short = rev.get(..12).unwrap_or_default();
|
||||
let rev_short = rev.get(..8).unwrap_or_default();
|
||||
println!("cargo:rustc-env=GIT_REV={}", rev_short);
|
||||
|
||||
// Combined version
|
||||
let version = if let Some(exact) = exact_tag {
|
||||
exact
|
||||
} else if &branch != "master" {
|
||||
format!("{}-{} ({})", last_tag, rev_short, branch)
|
||||
} else {
|
||||
format!("{}-{}", last_tag, rev_short)
|
||||
};
|
||||
println!("cargo:rustc-env=GIT_VERSION={}", version);
|
||||
|
||||
// To access these values, use:
|
||||
// env!("GIT_EXACT_TAG")
|
||||
// env!("GIT_LAST_TAG")
|
||||
// env!("GIT_BRANCH")
|
||||
// env!("GIT_REV")
|
||||
// env!("GIT_VERSION")
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
nightly-2019-01-26
|
||||
nightly-2019-03-14
|
||||
|
||||
+42
-23
@@ -15,8 +15,8 @@ use crate::mail;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
if CONFIG.admin_token().is_none() {
|
||||
return Vec::new();
|
||||
if CONFIG.admin_token().is_none() && !CONFIG.disable_admin_token() {
|
||||
return routes![admin_disabled];
|
||||
}
|
||||
|
||||
routes![
|
||||
@@ -26,21 +26,28 @@ pub fn routes() -> Vec<Route> {
|
||||
invite_user,
|
||||
delete_user,
|
||||
deauth_user,
|
||||
update_revision_users,
|
||||
post_config,
|
||||
delete_config,
|
||||
]
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn admin_disabled() -> &'static str {
|
||||
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
|
||||
}
|
||||
|
||||
const COOKIE_NAME: &str = "BWRS_ADMIN";
|
||||
const ADMIN_PATH: &str = "/admin";
|
||||
|
||||
const BASE_TEMPLATE: &str = "admin/base";
|
||||
const VERSION: Option<&str> = option_env!("GIT_VERSION");
|
||||
|
||||
#[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", "error": msg});
|
||||
let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg});
|
||||
|
||||
// Return the page
|
||||
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
|
||||
@@ -83,22 +90,24 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
|
||||
fn _validate_token(token: &str) -> bool {
|
||||
match CONFIG.admin_token().as_ref() {
|
||||
None => false,
|
||||
Some(t) => t == token,
|
||||
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdminTemplateData {
|
||||
users: Vec<Value>,
|
||||
page_content: String,
|
||||
version: Option<&'static str>,
|
||||
users: Vec<Value>,
|
||||
config: Value,
|
||||
}
|
||||
|
||||
impl AdminTemplateData {
|
||||
fn new(users: Vec<Value>) -> Self {
|
||||
Self {
|
||||
users,
|
||||
page_content: String::from("admin/page"),
|
||||
version: VERSION,
|
||||
users,
|
||||
config: CONFIG.prepare_json(),
|
||||
}
|
||||
}
|
||||
@@ -141,7 +150,7 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||
let org_name = "bitwarden_rs";
|
||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
||||
} else {
|
||||
let mut invitation = Invitation::new(data.email);
|
||||
let invitation = Invitation::new(data.email);
|
||||
invitation.save(&conn)
|
||||
}
|
||||
}
|
||||
@@ -163,11 +172,17 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
None => err!("User doesn't exist"),
|
||||
};
|
||||
|
||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||
user.reset_security_stamp();
|
||||
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
#[post("/users/update_revision")]
|
||||
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
User::update_all_revisions(&conn)
|
||||
}
|
||||
|
||||
#[post("/config", data = "<data>")]
|
||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||
let data: ConfigBuilder = data.into_inner();
|
||||
@@ -185,25 +200,29 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
|
||||
type Error = &'static str;
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
let mut cookies = request.cookies();
|
||||
if CONFIG.disable_admin_token() {
|
||||
Outcome::Success(AdminToken {})
|
||||
} else {
|
||||
let mut cookies = request.cookies();
|
||||
|
||||
let access_token = match cookies.get(COOKIE_NAME) {
|
||||
Some(cookie) => cookie.value(),
|
||||
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
|
||||
};
|
||||
let access_token = match cookies.get(COOKIE_NAME) {
|
||||
Some(cookie) => cookie.value(),
|
||||
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
|
||||
};
|
||||
|
||||
let ip = match request.guard::<ClientIp>() {
|
||||
Outcome::Success(ip) => ip.ip,
|
||||
_ => err_handler!("Error getting Client IP"),
|
||||
};
|
||||
let ip = match request.guard::<ClientIp>() {
|
||||
Outcome::Success(ip) => ip.ip,
|
||||
_ => err_handler!("Error getting Client IP"),
|
||||
};
|
||||
|
||||
if decode_admin(access_token).is_err() {
|
||||
// Remove admin cookie
|
||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||
error!("Invalid or expired admin JWT. IP: {}.", ip);
|
||||
return Outcome::Forward(());
|
||||
if decode_admin(access_token).is_err() {
|
||||
// Remove admin cookie
|
||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||
error!("Invalid or expired admin JWT. IP: {}.", ip);
|
||||
return Outcome::Forward(());
|
||||
}
|
||||
|
||||
Outcome::Success(AdminToken {})
|
||||
}
|
||||
|
||||
Outcome::Success(AdminToken {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +322,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
Device::delete_all_by_user(&user.uuid, &conn)?;
|
||||
user.reset_security_stamp();
|
||||
user.save(&conn)
|
||||
}
|
||||
|
||||
@@ -74,10 +74,10 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let user_json = headers.user.to_json(&conn);
|
||||
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
|
||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||
|
||||
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
|
||||
let collections_json: Vec<Value> = collections.iter().map(|c| c.to_json()).collect();
|
||||
let collections_json: Vec<Value> = collections.iter().map(Collection::to_json).collect();
|
||||
|
||||
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
|
||||
let ciphers_json: Vec<Value> = ciphers
|
||||
@@ -608,7 +608,7 @@ fn share_cipher_by_uuid(
|
||||
None => err!("Invalid collection ID provided"),
|
||||
Some(collection) => {
|
||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn)?;
|
||||
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn)?;
|
||||
shared_to_collection = true;
|
||||
} else {
|
||||
err!("No rights to modify the collection")
|
||||
@@ -854,11 +854,7 @@ fn move_cipher_selected(data: JsonUpcase<MoveCipherData>, headers: Headers, conn
|
||||
// Move cipher
|
||||
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?;
|
||||
|
||||
nt.send_cipher_update(
|
||||
UpdateType::CipherUpdate,
|
||||
&cipher,
|
||||
&[user_uuid.clone()]
|
||||
);
|
||||
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &[user_uuid.clone()]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
|
||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
|
||||
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
|
||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": folders_json,
|
||||
|
||||
+9
-8
@@ -33,10 +33,10 @@ use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
|
||||
use crate::auth::Headers;
|
||||
use crate::db::DbConn;
|
||||
use crate::error::Error;
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
||||
@@ -137,12 +137,13 @@ fn hibp_breach(username: String) -> JsonResult {
|
||||
|
||||
use reqwest::{header::USER_AGENT, Client};
|
||||
|
||||
let value: Value = Client::new()
|
||||
.get(&url)
|
||||
.header(USER_AGENT, user_agent)
|
||||
.send()?
|
||||
.error_for_status()?
|
||||
.json()?;
|
||||
let res = Client::new().get(&url).header(USER_AGENT, user_agent).send()?;
|
||||
|
||||
// If we get a 404, return a 404, it means no breached accounts
|
||||
if res.status() == 404 {
|
||||
return Err(Error::empty().with_code(404));
|
||||
}
|
||||
|
||||
let value: Value = res.error_for_status()?.json()?;
|
||||
Ok(Json(value))
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ struct NewCollectionData {
|
||||
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
|
||||
let data: OrgData = data.into_inner().data;
|
||||
|
||||
let mut org = Organization::new(data.Name, data.BillingEmail);
|
||||
let org = Organization::new(data.Name, data.BillingEmail);
|
||||
let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone());
|
||||
let mut collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
||||
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
|
||||
|
||||
user_org.key = data.Key;
|
||||
user_org.access_all = true;
|
||||
@@ -221,7 +221,7 @@ fn post_organization_collections(
|
||||
None => err!("Can't find organization details"),
|
||||
};
|
||||
|
||||
let mut collection = Collection::new(org.uuid.clone(), data.Name);
|
||||
let collection = Collection::new(org.uuid.clone(), data.Name);
|
||||
collection.save(&conn)?;
|
||||
|
||||
Ok(Json(collection.to_json()))
|
||||
@@ -484,7 +484,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
}
|
||||
|
||||
if !CONFIG.mail_enabled() {
|
||||
let mut invitation = Invitation::new(email.clone());
|
||||
let invitation = Invitation::new(email.clone());
|
||||
invitation.save(&conn)?;
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
|
||||
Some(headers.user.email),
|
||||
)?;
|
||||
} else {
|
||||
let mut invitation = Invitation::new(user.email.clone());
|
||||
let invitation = Invitation::new(user.email.clone());
|
||||
invitation.save(&conn)?;
|
||||
}
|
||||
|
||||
@@ -851,7 +851,7 @@ fn post_org_import(
|
||||
.Collections
|
||||
.into_iter()
|
||||
.map(|coll| {
|
||||
let mut collection = Collection::new(org_id.clone(), coll.Name);
|
||||
let collection = Collection::new(org_id.clone(), coll.Name);
|
||||
if collection.save(&conn).is_err() {
|
||||
err!("Failed to create Collection");
|
||||
}
|
||||
|
||||
+169
-110
File diff suppressed because it is too large
Load Diff
+74
-33
@@ -8,7 +8,7 @@ use rocket::Route;
|
||||
|
||||
use reqwest::{header::HeaderMap, Client, Response};
|
||||
|
||||
use rocket::http::{Cookie};
|
||||
use rocket::http::Cookie;
|
||||
|
||||
use regex::Regex;
|
||||
use soup::prelude::*;
|
||||
@@ -22,25 +22,54 @@ pub fn routes() -> Vec<Route> {
|
||||
|
||||
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
|
||||
|
||||
const ALLOWED_CHARS: &str = "_-.";
|
||||
|
||||
lazy_static! {
|
||||
// Reuse the client between requests
|
||||
static ref CLIENT: Client = Client::builder()
|
||||
.gzip(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
|
||||
.default_headers(_header_map())
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn is_valid_domain(domain: &str) -> bool {
|
||||
// Don't allow empty or too big domains or path traversal
|
||||
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only alphanumeric or specific characters
|
||||
for c in domain.chars() {
|
||||
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[get("/<domain>/icon.png")]
|
||||
fn icon(domain: String) -> Content<Vec<u8>> {
|
||||
let icon_type = ContentType::new("image", "x-icon");
|
||||
|
||||
// Validate the domain to avoid directory traversal attacks
|
||||
if domain.contains('/') || domain.contains("..") {
|
||||
if !is_valid_domain(&domain) {
|
||||
warn!("Invalid domain: {:#?}", domain);
|
||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||
}
|
||||
|
||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||
info!("Icon blacklist enabled: {:#?}", blacklist);
|
||||
|
||||
let regex = Regex::new(&blacklist).expect("Valid Regex");
|
||||
|
||||
if regex.is_match(&domain) {
|
||||
warn!("Blacklisted domain: {:#?}", domain);
|
||||
return Content(icon_type, FALLBACK_ICON.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
let icon = get_icon(&domain);
|
||||
|
||||
Content(icon_type, icon)
|
||||
@@ -132,11 +161,17 @@ fn icon_is_expired(path: &str) -> bool {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IconList {
|
||||
struct Icon {
|
||||
priority: u8,
|
||||
href: String,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
fn new(priority: u8, href: String) -> Self {
|
||||
Self { href, priority }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
|
||||
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
|
||||
/// This does not mean that that location does exists, but it is the default location browser use.
|
||||
@@ -149,13 +184,13 @@ struct IconList {
|
||||
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
|
||||
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
|
||||
/// ```
|
||||
fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||
fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
|
||||
// Default URL with secure and insecure schemes
|
||||
let ssldomain = format!("https://{}", domain);
|
||||
let httpdomain = format!("http://{}", domain);
|
||||
|
||||
// Create the iconlist
|
||||
let mut iconlist: Vec<IconList> = Vec::new();
|
||||
let mut iconlist: Vec<Icon> = Vec::new();
|
||||
|
||||
// Create the cookie_str to fill it all the cookies from the response
|
||||
// These cookies can be used to request/download the favicon image.
|
||||
@@ -167,13 +202,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
|
||||
let url = content.url().clone();
|
||||
let raw_cookies = content.headers().get_all("set-cookie");
|
||||
cookie_str = raw_cookies.iter().map(|raw_cookie| {
|
||||
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
|
||||
format!("{}={}; ", cookie.name(), cookie.value())
|
||||
}).collect::<String>();
|
||||
cookie_str = raw_cookies
|
||||
.iter()
|
||||
.map(|raw_cookie| {
|
||||
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
|
||||
format!("{}={}; ", cookie.name(), cookie.value())
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
// Add the default favicon.ico to the list with the domain the content responded from.
|
||||
iconlist.push(IconList { priority: 35, href: url.join("/favicon.ico").unwrap().into_string() });
|
||||
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
|
||||
|
||||
let soup = Soup::from_reader(content)?;
|
||||
// Search for and filter
|
||||
@@ -185,15 +223,17 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||
|
||||
// Loop through all the found icons and determine it's priority
|
||||
for favicon in favicons {
|
||||
let sizes = favicon.get("sizes").unwrap_or_default();
|
||||
let href = url.join(&favicon.get("href").unwrap_or_default()).unwrap().into_string();
|
||||
let priority = get_icon_priority(&href, &sizes);
|
||||
let sizes = favicon.get("sizes");
|
||||
let href = favicon.get("href").expect("Missing href");
|
||||
let full_href = url.join(&href).unwrap().into_string();
|
||||
|
||||
iconlist.push(IconList { priority, href })
|
||||
let priority = get_icon_priority(&full_href, sizes);
|
||||
|
||||
iconlist.push(Icon::new(priority, full_href))
|
||||
}
|
||||
} else {
|
||||
// Add the default favicon.ico to the list with just the given domain
|
||||
iconlist.push(IconList { priority: 35, href: format!("{}/favicon.ico", ssldomain) });
|
||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||
}
|
||||
|
||||
// Sort the iconlist by priority
|
||||
@@ -204,12 +244,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
|
||||
}
|
||||
|
||||
fn get_page(url: &str) -> Result<Response, Error> {
|
||||
//CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
|
||||
get_page_with_cookies(url, "")
|
||||
}
|
||||
|
||||
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
|
||||
CLIENT.get(url).header("cookie", cookie_str).send()?.error_for_status().map_err(Into::into)
|
||||
CLIENT
|
||||
.get(url)
|
||||
.header("cookie", cookie_str)
|
||||
.send()?
|
||||
.error_for_status()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns a Integer with the priority of the type of the icon which to prefer.
|
||||
@@ -224,7 +268,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
|
||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
||||
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||
/// ```
|
||||
fn get_icon_priority(href: &str, sizes: &str) -> u8 {
|
||||
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
||||
// Check if there is a dimension set
|
||||
let (width, height) = parse_sizes(sizes);
|
||||
|
||||
@@ -272,19 +316,19 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 {
|
||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||
/// ```
|
||||
fn parse_sizes(sizes: &str) -> (u16, u16) {
|
||||
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
||||
let mut width: u16 = 0;
|
||||
let mut height: u16 = 0;
|
||||
|
||||
if !sizes.is_empty() {
|
||||
if let Some(sizes) = sizes {
|
||||
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
|
||||
None => {},
|
||||
None => {}
|
||||
Some(dimensions) => {
|
||||
if dimensions.len() >= 3 {
|
||||
width = dimensions[1].parse::<u16>().unwrap_or_default();
|
||||
height = dimensions[2].parse::<u16>().unwrap_or_default();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,21 +336,18 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
|
||||
}
|
||||
|
||||
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
|
||||
let (mut iconlist, cookie_str) = get_icon_url(&domain)?;
|
||||
let (iconlist, cookie_str) = get_icon_url(&domain)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
iconlist.truncate(5);
|
||||
for icon in iconlist {
|
||||
let url = icon.href;
|
||||
info!("Downloading icon for {} via {}...", domain, url);
|
||||
match get_page_with_cookies(&url, &cookie_str) {
|
||||
for icon in iconlist.iter().take(5) {
|
||||
match get_page_with_cookies(&icon.href, &cookie_str) {
|
||||
Ok(mut res) => {
|
||||
info!("Download finished for {}", url);
|
||||
info!("Downloaded icon from {}", icon.href);
|
||||
res.copy_to(&mut buffer)?;
|
||||
break;
|
||||
},
|
||||
Err(_) => info!("Download failed for {}", url),
|
||||
}
|
||||
Err(_) => info!("Download failed for {}", icon.href),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+28
-38
@@ -118,7 +118,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
|
||||
None => Device::new(device_id, user.uuid.clone(), device_name, device_type),
|
||||
};
|
||||
|
||||
let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
|
||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
|
||||
|
||||
// Common
|
||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
||||
@@ -152,62 +152,45 @@ fn twofactor_auth(
|
||||
conn: &DbConn,
|
||||
) -> ApiResult<Option<String>> {
|
||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||
|
||||
// No twofactor token if twofactor is disabled
|
||||
if twofactors.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
|
||||
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
||||
|
||||
let twofactor_code = match data.two_factor_token {
|
||||
Some(ref code) => code,
|
||||
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||
};
|
||||
|
||||
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
|
||||
let selected_twofactor = twofactors.into_iter().filter(|tf| tf.type_ == selected_id).nth(0);
|
||||
|
||||
use crate::api::core::two_factor as _tf;
|
||||
use crate::crypto::ct_eq;
|
||||
|
||||
let selected_data = _selected_data(selected_twofactor);
|
||||
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||
|
||||
match TwoFactorType::from_i32(selected_id) {
|
||||
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||
|
||||
match TwoFactorType::from_i32(provider) {
|
||||
Some(TwoFactorType::Remember) => {
|
||||
match device.twofactor_remember {
|
||||
Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
|
||||
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
||||
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||
}
|
||||
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||
}
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Authenticator) => {
|
||||
let twofactor = match twofactor {
|
||||
Some(tf) => tf,
|
||||
None => err!("TOTP not enabled"),
|
||||
};
|
||||
|
||||
let totp_code: u64 = match twofactor_code.parse() {
|
||||
Ok(code) => code,
|
||||
_ => err!("Invalid TOTP code"),
|
||||
};
|
||||
|
||||
if !twofactor.check_totp_code(totp_code) {
|
||||
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
|
||||
}
|
||||
}
|
||||
|
||||
Some(TwoFactorType::U2f) => {
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
|
||||
}
|
||||
|
||||
Some(TwoFactorType::YubiKey) => {
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
|
||||
}
|
||||
|
||||
_ => err!("Invalid two factor provider"),
|
||||
}
|
||||
|
||||
if data.two_factor_remember.unwrap_or(0) == 1 {
|
||||
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||
Ok(Some(device.refresh_twofactor_remember()))
|
||||
} else {
|
||||
device.delete_twofactor_remember();
|
||||
@@ -215,6 +198,13 @@ fn twofactor_auth(
|
||||
}
|
||||
}
|
||||
|
||||
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||
match tf {
|
||||
Some(tf) => Ok(tf.data),
|
||||
None => err!("Two factor doesn't exist"),
|
||||
}
|
||||
}
|
||||
|
||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||
use crate::api::core::two_factor;
|
||||
|
||||
|
||||
+6
-3
@@ -47,10 +47,13 @@ impl NumberOrString {
|
||||
}
|
||||
}
|
||||
|
||||
fn into_i32(self) -> Option<i32> {
|
||||
fn into_i32(self) -> ApiResult<i32> {
|
||||
use std::num::ParseIntError as PIE;
|
||||
match self {
|
||||
NumberOrString::Number(n) => Some(n),
|
||||
NumberOrString::String(s) => s.parse().ok(),
|
||||
NumberOrString::Number(n) => Ok(n),
|
||||
NumberOrString::String(s) => s
|
||||
.parse()
|
||||
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ fn serialize(val: Value) -> Vec<u8> {
|
||||
|
||||
fn serialize_date(date: NaiveDateTime) -> Value {
|
||||
let seconds: i64 = date.timestamp();
|
||||
let nanos: i64 = date.timestamp_subsec_nanos() as i64;
|
||||
let nanos: i64 = date.timestamp_subsec_nanos().into();
|
||||
let timestamp = nanos << 34 | seconds;
|
||||
|
||||
let bs = timestamp.to_be_bytes();
|
||||
@@ -230,7 +230,7 @@ pub struct WebSocketUsers {
|
||||
}
|
||||
|
||||
impl WebSocketUsers {
|
||||
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
|
||||
fn send_update(&self, user_uuid: &str, data: &[u8]) -> ws::Result<()> {
|
||||
if let Some(user) = self.map.get(user_uuid) {
|
||||
for sender in user.iter() {
|
||||
sender.send(data)?;
|
||||
@@ -249,7 +249,7 @@ impl WebSocketUsers {
|
||||
ut,
|
||||
);
|
||||
|
||||
self.send_update(&user.uuid.clone(), &data).ok();
|
||||
self.send_update(&user.uuid, &data).ok();
|
||||
}
|
||||
|
||||
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
|
||||
|
||||
+2
-8
@@ -21,17 +21,11 @@ lazy_static! {
|
||||
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
|
||||
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 from {}\n Error: {}",
|
||||
CONFIG.private_rsa_key(), e
|
||||
),
|
||||
Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
|
||||
};
|
||||
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) {
|
||||
Ok(key) => key,
|
||||
Err(e) => panic!(
|
||||
"Error loading public RSA Key from {}\n Error: {}",
|
||||
CONFIG.public_rsa_key(), e
|
||||
),
|
||||
Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+96
-17
@@ -12,6 +12,8 @@ lazy_static! {
|
||||
pub static ref CONFIG_FILE: String = get_env("CONFIG_FILE").unwrap_or_else(|| "data/config.json".into());
|
||||
}
|
||||
|
||||
pub type Pass = String;
|
||||
|
||||
macro_rules! make_config {
|
||||
($(
|
||||
$(#[doc = $groupdoc:literal])?
|
||||
@@ -60,12 +62,25 @@ macro_rules! make_config {
|
||||
/// Merges the values of both builders into a new builder.
|
||||
/// If both have the same element, `other` wins.
|
||||
fn merge(&self, other: &Self) -> Self {
|
||||
let mut overrides = Vec::new();
|
||||
let mut builder = self.clone();
|
||||
$($(
|
||||
if let v @Some(_) = &other.$name {
|
||||
builder.$name = v.clone();
|
||||
|
||||
if self.$name.is_some() {
|
||||
overrides.push(stringify!($name).to_uppercase());
|
||||
}
|
||||
}
|
||||
)+)+
|
||||
|
||||
if !overrides.is_empty() {
|
||||
// We can't use warn! here because logging isn't setup yet.
|
||||
println!("[WARNING] The following environment variables are being overriden by the config file,");
|
||||
println!("[WARNING] please use the admin panel to make changes to them:");
|
||||
println!("[WARNING] {}\n", overrides.join(", "));
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
@@ -114,6 +129,7 @@ macro_rules! make_config {
|
||||
|
||||
fn _get_form_type(rust_type: &str) -> &'static str {
|
||||
match rust_type {
|
||||
"Pass" => "password",
|
||||
"String" => "text",
|
||||
"bool" => "checkbox",
|
||||
_ => "number"
|
||||
@@ -208,28 +224,31 @@ make_config! {
|
||||
|
||||
/// General settings
|
||||
settings {
|
||||
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value
|
||||
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
|
||||
/// and port, if it's different than the default. Some server functions don't work correctly without this value
|
||||
domain: String, true, def, "http://localhost".to_string();
|
||||
/// PRIVATE |> Domain set
|
||||
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
|
||||
domain_set: bool, false, def, false;
|
||||
/// Enable web vault
|
||||
web_vault_enabled: bool, false, def, true;
|
||||
|
||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
||||
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||
/// otherwise it will delete them and they won't be downloaded again.
|
||||
disable_icon_download: bool, true, def, false;
|
||||
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
|
||||
signups_allowed: bool, true, def, true;
|
||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
||||
invitations_allowed: bool, true, def, true;
|
||||
/// Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value
|
||||
/// Password iterations |> Number of server-side passwords hashing iterations.
|
||||
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
||||
password_iterations: i32, true, def, 100_000;
|
||||
/// Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint
|
||||
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
|
||||
/// Otherwise, if email is disabled, there is no way to see the password hint
|
||||
show_password_hint: bool, true, def, true;
|
||||
|
||||
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
||||
admin_token: String, true, option;
|
||||
admin_token: Pass, true, option;
|
||||
},
|
||||
|
||||
/// Advanced settings
|
||||
@@ -238,14 +257,33 @@ make_config! {
|
||||
icon_cache_ttl: u64, true, def, 2_592_000;
|
||||
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
|
||||
icon_cache_negttl: u64, true, def, 259_200;
|
||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||
icon_download_timeout: u64, true, def, 10;
|
||||
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
|
||||
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
||||
icon_blacklist_regex: String, true, option;
|
||||
|
||||
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server
|
||||
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
||||
/// Note that the checkbox would still be present, but ignored.
|
||||
disable_2fa_remember: bool, true, def, false;
|
||||
|
||||
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
|
||||
/// ONLY use this during development, as it can slow down the server
|
||||
reload_templates: bool, true, def, false;
|
||||
|
||||
/// Log routes at launch (Dev)
|
||||
log_mounts: bool, true, def, false;
|
||||
/// Enable extended logging
|
||||
extended_logging: bool, false, def, true;
|
||||
/// Log file path
|
||||
log_file: String, false, option;
|
||||
|
||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
|
||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||
enable_db_wal: bool, false, def, true;
|
||||
|
||||
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||
disable_admin_token: bool, true, def, false;
|
||||
},
|
||||
|
||||
/// Yubikey settings
|
||||
@@ -255,7 +293,7 @@ make_config! {
|
||||
/// Client ID
|
||||
yubico_client_id: String, true, option;
|
||||
/// Secret Key
|
||||
yubico_secret_key: String, true, option;
|
||||
yubico_secret_key: Pass, true, option;
|
||||
/// Server
|
||||
yubico_server: String, true, option;
|
||||
},
|
||||
@@ -268,8 +306,10 @@ make_config! {
|
||||
smtp_host: String, true, option;
|
||||
/// Enable SSL
|
||||
smtp_ssl: bool, true, def, true;
|
||||
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
|
||||
smtp_explicit_tls: bool, true, def, false;
|
||||
/// Port
|
||||
smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25};
|
||||
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
|
||||
/// From Address
|
||||
smtp_from: String, true, def, String::new();
|
||||
/// From Name
|
||||
@@ -277,11 +317,17 @@ make_config! {
|
||||
/// Username
|
||||
smtp_username: String, true, option;
|
||||
/// Password
|
||||
smtp_password: String, true, option;
|
||||
smtp_password: Pass, true, option;
|
||||
},
|
||||
}
|
||||
|
||||
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
if let Some(ref token) = cfg.admin_token {
|
||||
if token.trim().is_empty() {
|
||||
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
||||
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
||||
}
|
||||
@@ -416,21 +462,27 @@ fn load_templates(path: &str) -> Handlebars {
|
||||
let mut hb = Handlebars::new();
|
||||
// Error on missing params
|
||||
hb.set_strict_mode(true);
|
||||
// Register helpers
|
||||
hb.register_helper("case", Box::new(CaseHelper));
|
||||
hb.register_helper("jsesc", Box::new(JsEscapeHelper));
|
||||
|
||||
macro_rules! reg {
|
||||
($name:expr) => {{
|
||||
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
|
||||
hb.register_template_string($name, template).unwrap();
|
||||
}};
|
||||
($name:expr, $ext:expr) => {{
|
||||
reg!($name);
|
||||
reg!(concat!($name, $ext));
|
||||
}};
|
||||
}
|
||||
|
||||
// First register default templates here
|
||||
reg!("email/invite_accepted");
|
||||
reg!("email/invite_confirmed");
|
||||
reg!("email/pw_hint_none");
|
||||
reg!("email/pw_hint_some");
|
||||
reg!("email/send_org_invite");
|
||||
reg!("email/invite_accepted", ".html");
|
||||
reg!("email/invite_confirmed", ".html");
|
||||
reg!("email/pw_hint_none", ".html");
|
||||
reg!("email/pw_hint_some", ".html");
|
||||
reg!("email/send_org_invite", ".html");
|
||||
|
||||
reg!("admin/base");
|
||||
reg!("admin/login");
|
||||
@@ -444,7 +496,6 @@ fn load_templates(path: &str) -> Handlebars {
|
||||
hb
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CaseHelper;
|
||||
|
||||
impl HelperDef for CaseHelper {
|
||||
@@ -468,3 +519,31 @@ impl HelperDef for CaseHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JsEscapeHelper;
|
||||
|
||||
impl HelperDef for JsEscapeHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'reg, 'rc>,
|
||||
_: &'reg Handlebars,
|
||||
_: &Context,
|
||||
_: &mut RenderContext<'reg>,
|
||||
out: &mut Output,
|
||||
) -> HelperResult {
|
||||
let param = h
|
||||
.param(0)
|
||||
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
|
||||
|
||||
let value = param
|
||||
.value()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
|
||||
|
||||
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
||||
let quoted_value = format!(""{}"", escaped_value);
|
||||
|
||||
out.write("ed_value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,3 +36,12 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
|
||||
|
||||
array
|
||||
}
|
||||
|
||||
//
|
||||
// Constant time compare
|
||||
//
|
||||
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
|
||||
use ring::constant_time::verify_slices_are_equal;
|
||||
|
||||
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ use crate::error::MapResult;
|
||||
|
||||
/// Database methods
|
||||
impl Collection {
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
self.update_users_revision(conn);
|
||||
|
||||
diesel::replace_into(collections::table)
|
||||
.values(&*self)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving collection")
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ use crate::error::MapResult;
|
||||
|
||||
/// Database methods
|
||||
impl Organization {
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
UserOrganization::find_by_org(&self.uuid, conn)
|
||||
.iter()
|
||||
.for_each(|user_org| {
|
||||
@@ -221,7 +221,7 @@ impl Organization {
|
||||
});
|
||||
|
||||
diesel::replace_into(organizations::table)
|
||||
.values(&*self)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error saving organization")
|
||||
}
|
||||
@@ -323,11 +323,11 @@ impl UserOrganization {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||
pub fn save(&self, conn: &DbConn) -> EmptyResult {
|
||||
User::update_uuid_revision(&self.user_uuid, conn);
|
||||
|
||||
diesel::replace_into(users_organizations::table)
|
||||
.values(&*self)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
.map_res("Error adding user to organization")
|
||||
}
|
||||
|
||||
@@ -42,21 +42,6 @@ impl TwoFactor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_totp_code(&self, totp_code: u64) -> bool {
|
||||
let totp_secret = self.data.as_bytes();
|
||||
|
||||
use data_encoding::BASE32;
|
||||
use oath::{totp_raw_now, HashType};
|
||||
|
||||
let decoded_secret = match BASE32.decode(totp_secret) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
||||
generated == totp_code
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user