forked from trashmodern/vaultwarden
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03172a6cd7 | |||
| 819622e310 | |||
| 970863ffb1 | |||
| e876d3077a | |||
| 99d6742fac | |||
| 75615bb5c8 | |||
| e271b246f3 | |||
| 6378d96d1a | |||
| c722256cbd | |||
| 8ff50481e5 | |||
| be4e6c6f0c | |||
| 2f892cb866 | |||
| 4f6f510bd4 | |||
| dae92b9018 | |||
| dde7c0d99b | |||
| 79fccccad7 | |||
| 470ad14616 | |||
| 8d13e759fa | |||
| 3bba02b364 | |||
| 251c5c2348 | |||
| f718827693 | |||
| 869352c361 | |||
| ca31f117d5 | |||
| 1cb67eee69 | |||
| e88d8c856d | |||
| ec37004dfe | |||
| 03ce42e1cf | |||
| 3f56730b8a | |||
| 57701d5213 | |||
| f920441b28 | |||
| 203fb2e3e7 | |||
| 3c662de4f2 | |||
| b1d1926249 | |||
| c5dd1a03be | |||
| df598d7208 | |||
| a0ae032ea7 | |||
| 35b4ad69bd | |||
| dfb348d630 | |||
| 22786c8c9d | |||
| a1ffa4c28d | |||
| 9f8183deb0 | |||
| ea600ab2b8 | |||
| 83da757dfb | |||
| d84d8d756f | |||
| 4fcdf33621 | |||
| 400a17a1ce | |||
| 15833e8d95 | |||
| 7d01947173 | |||
| 6aab2ae6c8 | |||
| 64ac81b9ee | |||
| 7c316fc19a | |||
| 1c45c2ec3a | |||
| 0905355629 | |||
| f24e754ff7 | |||
| 0260667f7a | |||
| 7983ce4f13 | |||
| 5fc0472d88 | |||
| 410ee9f1f7 | |||
| 538dc00234 | |||
| 515c84d74d | |||
| f72efa899e | |||
| 483066b9a0 | |||
| 57850a3379 | |||
| 3b09750b76 | |||
| 0da4a8fc8a |
@@ -1,13 +1,40 @@
|
||||
## Bitwarden_RS Configuration File
|
||||
## Uncomment any of the following lines to change the defaults
|
||||
|
||||
## Main data folder
|
||||
# DATA_FOLDER=data
|
||||
|
||||
## Individual folders, these override %DATA_FOLDER%
|
||||
# DATABASE_URL=data/db.sqlite3
|
||||
# PRIVATE_RSA_KEY=data/private_rsa_key.der
|
||||
# PUBLIC_RSA_KEY=data/public_rsa_key.der
|
||||
# RSA_KEY_FILENAME=data/rsa_key
|
||||
# ICON_CACHE_FOLDER=data/icon_cache
|
||||
# ATTACHMENTS_FOLDER=data/attachments
|
||||
|
||||
# true for yes, anything else for no
|
||||
SIGNUPS_ALLOWED=true
|
||||
## Web vault settings
|
||||
# WEB_VAULT_FOLDER=web-vault/
|
||||
# WEB_VAULT_ENABLED=true
|
||||
|
||||
# ROCKET_ENV=production
|
||||
## Controls if new users can register
|
||||
# SIGNUPS_ALLOWED=true
|
||||
|
||||
## Use a local favicon extractor
|
||||
## Set to false to use bitwarden's official icon servers
|
||||
## Set to true to use the local version, which is not as smart,
|
||||
## but it doesn't send the cipher domains to bitwarden's servers
|
||||
# LOCAL_ICON_EXTRACTOR=false
|
||||
|
||||
## Controls the PBBKDF password iterations to apply on the server
|
||||
## The change only applies when the password is changed
|
||||
# PASSWORD_ITERATIONS=100000
|
||||
|
||||
## Domain settings
|
||||
## The domain must match the address from where you access the server
|
||||
## Unless you are using U2F, or having problems with attachments not downloading, there is no need to change this
|
||||
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
|
||||
# DOMAIN=https://bw.domain.tld:8443
|
||||
|
||||
## Rocket specific settings, check Rocket documentation to learn more
|
||||
# ROCKET_ENV=staging
|
||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||
# ROCKET_PORT=8000
|
||||
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Build instructions
|
||||
|
||||
## Dependencies
|
||||
- `Rust nightly` (strongly recommended to use [rustup](https://rustup.rs/))
|
||||
- `OpenSSL` (should be available in path, install through your system's package manager or use the [prebuilt binaries](https://wiki.openssl.org/index.php/Binaries))
|
||||
- `NodeJS` (required to build the web-vault, (install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
|
||||
|
||||
|
||||
## Run/Compile
|
||||
```sh
|
||||
# Compile and run
|
||||
cargo run
|
||||
# or just compile (binary located in target/release/bitwarden_rs)
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
When run, the server is accessible in [http://localhost:80](http://localhost:80).
|
||||
|
||||
### Install the web-vault
|
||||
Download the latest official release from the [releases page](https://github.com/bitwarden/web/releases) and extract it.
|
||||
|
||||
Modify `web-vault/settings.Production.json` to look like this:
|
||||
```json
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "/api",
|
||||
"identityUri": "/identity",
|
||||
"iconsUri": "/icons",
|
||||
"stripeKey": "",
|
||||
"braintreeKey": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, run the following from the `web-vault` directory:
|
||||
```sh
|
||||
npm install
|
||||
npx gulp dist:selfHosted
|
||||
```
|
||||
|
||||
Finally copy the contents of the `web-vault/dist` folder into the `bitwarden_rs/web-vault` folder.
|
||||
|
||||
# Configuration
|
||||
The available configuration options are documented in the default `.env` file, and they can be modified by uncommenting the desired options in that file or by setting their respective environment variables. Look at the README file for the main configuration options available.
|
||||
|
||||
Note: the environment variables override the values set in the `.env` file.
|
||||
|
||||
## How to recreate database schemas (for developers)
|
||||
Install diesel-cli with cargo:
|
||||
```sh
|
||||
cargo install diesel_cli --no-default-features --features sqlite-bundled
|
||||
```
|
||||
|
||||
Make sure that the correct path to the database is in the `.env` file.
|
||||
|
||||
If you want to modify the schemas, create a new migration with:
|
||||
```
|
||||
diesel migration generate <name>
|
||||
```
|
||||
|
||||
Modify the *.sql files, making sure that any changes are reverted in the down.sql file.
|
||||
|
||||
Apply the migrations and save the generated schemas as follows:
|
||||
```sh
|
||||
diesel migration redo
|
||||
|
||||
# This step should be done automatically when using diesel-cli > 1.3.0
|
||||
# diesel print-schema > src/db/schema.rs
|
||||
```
|
||||
Generated
+358
-293
File diff suppressed because it is too large
Load Diff
+22
-11
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "bitwarden_rs"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||
|
||||
[dependencies]
|
||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||
rocket = { version = "0.3.12", features = ["tls"] }
|
||||
rocket_codegen = "0.3.12"
|
||||
rocket_contrib = "0.3.12"
|
||||
rocket = { version = "0.3.14", features = ["tls"] }
|
||||
rocket_codegen = "0.3.14"
|
||||
rocket_contrib = "0.3.14"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.8.6"
|
||||
@@ -16,13 +16,13 @@ reqwest = "0.8.6"
|
||||
multipart = "0.14.2"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.64"
|
||||
serde_derive = "1.0.64"
|
||||
serde_json = "1.0.19"
|
||||
serde = "1.0.70"
|
||||
serde_derive = "1.0.70"
|
||||
serde_json = "1.0.22"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "~1.2.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "~1.2.0", features = ["sqlite"] }
|
||||
diesel = { version = "1.3.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||
|
||||
# Bundled SQLite
|
||||
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }
|
||||
@@ -34,7 +34,7 @@ ring = { version = "= 0.11.0", features = ["rsa_signing"] }
|
||||
uuid = { version = "0.6.5", features = ["v4"] }
|
||||
|
||||
# Date and time library for Rust
|
||||
chrono = "0.4.2"
|
||||
chrono = "0.4.4"
|
||||
|
||||
# TOTP library
|
||||
oath = "0.10.2"
|
||||
@@ -45,11 +45,22 @@ data-encoding = "2.1.1"
|
||||
# JWT library
|
||||
jsonwebtoken = "= 4.0.1"
|
||||
|
||||
# U2F library
|
||||
u2f = "0.1.2"
|
||||
|
||||
# A `dotenv` implementation for Rust
|
||||
dotenv = { version = "0.13.0", default-features = false }
|
||||
|
||||
# Lazy static macro
|
||||
lazy_static = "1.0.1"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.5"
|
||||
num-derive = "0.2.2"
|
||||
|
||||
[patch.crates-io]
|
||||
jsonwebtoken = { path = "libs/jsonwebtoken" } # Make jwt use ring 0.11, to match rocket
|
||||
# Make jwt use ring 0.11, to match rocket
|
||||
jsonwebtoken = { path = "libs/jsonwebtoken" }
|
||||
|
||||
# Version 0.1.2 from crates.io lacks a commit that fixes a certificate error
|
||||
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' }
|
||||
+8
-5
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM node:9-alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "1.26.0"
|
||||
ENV VAULT_VERSION "1.27.0"
|
||||
ENV URL "https://github.com/bitwarden/web/archive/v${VAULT_VERSION}.tar.gz"
|
||||
|
||||
RUN apk add --update-cache --upgrade \
|
||||
@@ -31,7 +31,7 @@ RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com
|
||||
########################## BUILD IMAGE ##########################
|
||||
# We need to use the Rust build image, because
|
||||
# we need the Rust compiler and Cargo tooling
|
||||
FROM rustlang/rust:nightly as build
|
||||
FROM rust as build
|
||||
|
||||
# Using bundled SQLite, no need to install it
|
||||
# RUN apt-get update && apt-get install -y\
|
||||
@@ -46,6 +46,7 @@ WORKDIR /app
|
||||
# Copies over *only* your manifests and vendored dependencies
|
||||
COPY ./Cargo.* ./
|
||||
COPY ./libs ./libs
|
||||
COPY ./rust-toolchain ./rust-toolchain
|
||||
|
||||
# Builds your dependencies and removes the
|
||||
# dummy project, except the target folder
|
||||
@@ -66,9 +67,12 @@ RUN cargo build --release
|
||||
# because we already have a binary built
|
||||
FROM debian:stretch-slim
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
|
||||
# Install needed libraries
|
||||
RUN apt-get update && apt-get install -y\
|
||||
openssl\
|
||||
ca-certificates\
|
||||
--no-install-recommends\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -79,10 +83,9 @@ EXPOSE 80
|
||||
# Copies the files from the context (env file and web-vault)
|
||||
# and the binary from the "build" stage to the current stage
|
||||
COPY .env .
|
||||
COPY Rocket.toml .
|
||||
COPY --from=vault /web-vault ./web-vault
|
||||
COPY --from=build app/target/release/bitwarden_rs .
|
||||
|
||||
# Configures the startup!
|
||||
# Use production to disable Rocket logging
|
||||
#CMD ROCKET_ENV=production ./bitwarden_rs
|
||||
CMD ROCKET_ENV=staging ./bitwarden_rs
|
||||
CMD ./bitwarden_rs
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[global.limits]
|
||||
json = 10485760 # 10 MiB
|
||||
@@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/db/schema.rs"
|
||||
@@ -0,0 +1,8 @@
|
||||
UPDATE users
|
||||
SET totp_secret = (
|
||||
SELECT twofactor.data FROM twofactor
|
||||
WHERE twofactor.type = 0
|
||||
AND twofactor.user_uuid = users.uuid
|
||||
);
|
||||
|
||||
DROP TABLE twofactor;
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE twofactor (
|
||||
uuid TEXT NOT NULL PRIMARY KEY,
|
||||
user_uuid TEXT NOT NULL REFERENCES users (uuid),
|
||||
type INTEGER NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
|
||||
UNIQUE (user_uuid, type)
|
||||
);
|
||||
|
||||
|
||||
INSERT INTO twofactor (uuid, user_uuid, type, enabled, data)
|
||||
SELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;
|
||||
|
||||
UPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty
|
||||
@@ -0,0 +1 @@
|
||||
nightly-2018-06-26
|
||||
+61
-14
@@ -3,11 +3,9 @@ use rocket_contrib::Json;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase};
|
||||
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
|
||||
use auth::Headers;
|
||||
|
||||
use util;
|
||||
|
||||
use CONFIG;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -15,7 +13,6 @@ use CONFIG;
|
||||
struct RegisterData {
|
||||
Email: String,
|
||||
Key: String,
|
||||
#[serde(deserialize_with = "util::upcase_deserialize")]
|
||||
Keys: Option<KeysData>,
|
||||
MasterPasswordHash: String,
|
||||
MasterPasswordHint: Option<String>,
|
||||
@@ -34,10 +31,10 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||
let data: RegisterData = data.into_inner().data;
|
||||
|
||||
if !CONFIG.signups_allowed {
|
||||
err!(format!("Signups not allowed"))
|
||||
err!("Signups not allowed")
|
||||
}
|
||||
|
||||
if let Some(_) = User::find_by_mail(&data.Email, &conn) {
|
||||
if User::find_by_mail(&data.Email, &conn).is_some() {
|
||||
err!("Email already exists")
|
||||
}
|
||||
|
||||
@@ -67,6 +64,28 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
|
||||
Ok(Json(headers.user.to_json(&conn)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ProfileData {
|
||||
#[serde(rename = "Culture")]
|
||||
_Culture: String, // Ignored, always use en-US
|
||||
MasterPasswordHint: Option<String>,
|
||||
Name: String,
|
||||
}
|
||||
|
||||
#[post("/accounts/profile", data = "<data>")]
|
||||
fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: ProfileData = data.into_inner().data;
|
||||
|
||||
let mut user = headers.user;
|
||||
|
||||
user.name = data.Name;
|
||||
user.password_hint = data.MasterPasswordHint;
|
||||
user.save(&conn);
|
||||
|
||||
Ok(Json(user.to_json(&conn)))
|
||||
}
|
||||
|
||||
#[get("/users/<uuid>/public-key")]
|
||||
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||
@@ -136,13 +155,39 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ChangeEmailData {
|
||||
struct EmailTokenData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
}
|
||||
|
||||
|
||||
#[post("/accounts/email-token", data = "<data>")]
|
||||
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: EmailTokenData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
if User::find_by_mail(&data.NewEmail, &conn).is_some() {
|
||||
err!("Email already in use");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ChangeEmailData {
|
||||
MasterPasswordHash: String,
|
||||
NewEmail: String,
|
||||
|
||||
Key: String,
|
||||
NewMasterPasswordHash: String,
|
||||
#[serde(rename = "Token")]
|
||||
_Token: NumberOrString,
|
||||
}
|
||||
|
||||
#[post("/accounts/email", data = "<data>")]
|
||||
fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: ChangeEmailData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
@@ -156,6 +201,10 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
|
||||
}
|
||||
|
||||
user.email = data.NewEmail;
|
||||
|
||||
user.set_password(&data.NewMasterPasswordHash);
|
||||
user.key = data.Key;
|
||||
|
||||
user.save(&conn);
|
||||
|
||||
Ok(())
|
||||
@@ -172,17 +221,15 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
|
||||
|
||||
// Delete ciphers and their attachments
|
||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||
match cipher.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting cipher")
|
||||
if cipher.delete(&conn).is_err() {
|
||||
err!("Failed deleting cipher")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
match f.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting folder")
|
||||
if f.delete(&conn).is_err() {
|
||||
err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-61
@@ -14,7 +14,6 @@ use data_encoding::HEXLOWER;
|
||||
use db::DbConn;
|
||||
use db::models::*;
|
||||
|
||||
use util;
|
||||
use crypto;
|
||||
|
||||
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase};
|
||||
@@ -157,24 +156,6 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
|
||||
}
|
||||
}
|
||||
|
||||
let uppercase_fields = data.Fields.map(|f| {
|
||||
let mut value = json!({});
|
||||
// Copy every field object and change the names to the correct case
|
||||
copy_values(&f, &mut value);
|
||||
value
|
||||
});
|
||||
|
||||
// TODO: ******* Backwards compat start **********
|
||||
// To remove backwards compatibility, just create an empty values object,
|
||||
// and remove the compat code from cipher::to_json
|
||||
let mut values = json!({
|
||||
"Name": data.Name,
|
||||
"Notes": data.Notes
|
||||
});
|
||||
|
||||
values["Fields"] = uppercase_fields.clone().unwrap_or(Value::Null);
|
||||
// TODO: ******* Backwards compat end **********
|
||||
|
||||
let type_data_opt = match data.Type {
|
||||
1 => data.Login,
|
||||
2 => data.SecureNote,
|
||||
@@ -183,19 +164,24 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
|
||||
_ => err!("Invalid type")
|
||||
};
|
||||
|
||||
let type_data = match type_data_opt {
|
||||
let mut type_data = match type_data_opt {
|
||||
Some(data) => data,
|
||||
None => err!("Data missing")
|
||||
};
|
||||
|
||||
// Copy the type data and change the names to the correct case
|
||||
copy_values(&type_data, &mut values);
|
||||
// TODO: ******* Backwards compat start **********
|
||||
// To remove backwards compatibility, just delete this code,
|
||||
// and remove the compat code from cipher::to_json
|
||||
type_data["Name"] = Value::String(data.Name.clone());
|
||||
type_data["Notes"] = data.Notes.clone().map(Value::String).unwrap_or(Value::Null);
|
||||
type_data["Fields"] = data.Fields.clone().unwrap_or(Value::Null);
|
||||
// TODO: ******* Backwards compat end **********
|
||||
|
||||
cipher.favorite = data.Favorite.unwrap_or(false);
|
||||
cipher.name = data.Name;
|
||||
cipher.notes = data.Notes;
|
||||
cipher.fields = uppercase_fields.map(|f| f.to_string());
|
||||
cipher.data = values.to_string();
|
||||
cipher.fields = data.Fields.map(|f| f.to_string());
|
||||
cipher.data = type_data.to_string();
|
||||
|
||||
cipher.save(&conn);
|
||||
|
||||
@@ -206,23 +192,6 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_values(from: &Value, to: &mut Value) {
|
||||
if let Some(map) = from.as_object() {
|
||||
for (key, val) in map {
|
||||
copy_values(val, &mut to[util::upcase_first(key)]);
|
||||
}
|
||||
} else if let Some(array) = from.as_array() {
|
||||
// Initialize array with null values
|
||||
*to = json!(vec![Value::Null; array.len()]);
|
||||
|
||||
for (index, val) in array.iter().enumerate() {
|
||||
copy_values(val, &mut to[index]);
|
||||
}
|
||||
} else {
|
||||
*to = from.clone();
|
||||
}
|
||||
}
|
||||
|
||||
use super::folders::FolderData;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -237,9 +206,9 @@ struct ImportData {
|
||||
#[allow(non_snake_case)]
|
||||
struct RelationsData {
|
||||
// Cipher id
|
||||
key: u32,
|
||||
Key: usize,
|
||||
// Folder id
|
||||
value: u32,
|
||||
Value: usize,
|
||||
}
|
||||
|
||||
|
||||
@@ -259,21 +228,18 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
|
||||
let mut relations_map = HashMap::new();
|
||||
|
||||
for relation in data.FolderRelationships {
|
||||
relations_map.insert(relation.key, relation.value);
|
||||
relations_map.insert(relation.Key, relation.Value);
|
||||
}
|
||||
|
||||
// Read and create the ciphers
|
||||
let mut index = 0;
|
||||
for cipher_data in data.Ciphers {
|
||||
for (index, cipher_data) in data.Ciphers.into_iter().enumerate() {
|
||||
let folder_uuid = relations_map.get(&index)
|
||||
.map(|i| folders[*i as usize].uuid.clone());
|
||||
.map(|i| folders[*i].uuid.clone());
|
||||
|
||||
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
|
||||
update_cipher_from_data(&mut cipher, cipher_data, &headers, true, &conn)?;
|
||||
|
||||
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok();
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -358,7 +324,6 @@ fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>,
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct ShareCipherData {
|
||||
#[serde(deserialize_with = "util::upcase_deserialize")]
|
||||
Cipher: CipherData,
|
||||
CollectionIds: Vec<String>,
|
||||
}
|
||||
@@ -382,8 +347,8 @@ fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: H
|
||||
None => err!("Organization id not provided"),
|
||||
Some(_) => {
|
||||
update_cipher_from_data(&mut cipher, data.Cipher, &headers, true, &conn)?;
|
||||
for collection in data.CollectionIds.iter() {
|
||||
match Collection::find_by_uuid(&collection, &conn) {
|
||||
for uuid in &data.CollectionIds {
|
||||
match Collection::find_by_uuid(uuid, &conn) {
|
||||
None => err!("Invalid collection ID provided"),
|
||||
Some(collection) => {
|
||||
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
|
||||
@@ -418,7 +383,8 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
|
||||
let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid);
|
||||
|
||||
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
|
||||
let name = field.headers.filename.unwrap(); // This is provided by the client, don't trust it
|
||||
// This is provided by the client, don't trust it
|
||||
let name = field.headers.filename.expect("No filename provided");
|
||||
|
||||
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
|
||||
let path = base_path.join(&file_name);
|
||||
@@ -428,11 +394,21 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
|
||||
.size_limit(None)
|
||||
.with_path(path) {
|
||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||
_ => return
|
||||
SaveResult::Full(other) => {
|
||||
println!("Attachment is not a file: {:?}", other);
|
||||
return;
|
||||
},
|
||||
SaveResult::Partial(_, reason) => {
|
||||
println!("Partial result: {:?}", reason);
|
||||
return;
|
||||
},
|
||||
SaveResult::Error(e) => {
|
||||
println!("Error: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
|
||||
println!("Attachment: {:#?}", attachment);
|
||||
attachment.save(&conn);
|
||||
}).expect("Error processing multipart data");
|
||||
|
||||
@@ -476,6 +452,11 @@ fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResu
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/delete-admin")]
|
||||
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
}
|
||||
|
||||
#[delete("/ciphers/<uuid>")]
|
||||
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
_delete_cipher_by_uuid(&uuid, &headers, &conn)
|
||||
@@ -567,17 +548,15 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
||||
|
||||
// Delete ciphers and their attachments
|
||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||
match cipher.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting cipher")
|
||||
if cipher.delete(&conn).is_err() {
|
||||
err!("Failed deleting cipher")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
match f.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting folder")
|
||||
if f.delete(&conn).is_err() {
|
||||
err!("Failed deleting folder")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -646,5 +646,121 @@
|
||||
"wiktionary.org"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 72,
|
||||
"Domains": [
|
||||
"airbnb.at",
|
||||
"airbnb.be",
|
||||
"airbnb.ca",
|
||||
"airbnb.ch",
|
||||
"airbnb.cl",
|
||||
"airbnb.co.cr",
|
||||
"airbnb.co.id",
|
||||
"airbnb.co.in",
|
||||
"airbnb.co.kr",
|
||||
"airbnb.co.nz",
|
||||
"airbnb.co.uk",
|
||||
"airbnb.co.ve",
|
||||
"airbnb.com",
|
||||
"airbnb.com.ar",
|
||||
"airbnb.com.au",
|
||||
"airbnb.com.bo",
|
||||
"airbnb.com.br",
|
||||
"airbnb.com.bz",
|
||||
"airbnb.com.co",
|
||||
"airbnb.com.ec",
|
||||
"airbnb.com.gt",
|
||||
"airbnb.com.hk",
|
||||
"airbnb.com.hn",
|
||||
"airbnb.com.mt",
|
||||
"airbnb.com.my",
|
||||
"airbnb.com.ni",
|
||||
"airbnb.com.pa",
|
||||
"airbnb.com.pe",
|
||||
"airbnb.com.py",
|
||||
"airbnb.com.sg",
|
||||
"airbnb.com.sv",
|
||||
"airbnb.com.tr",
|
||||
"airbnb.com.tw",
|
||||
"airbnb.cz",
|
||||
"airbnb.de",
|
||||
"airbnb.dk",
|
||||
"airbnb.es",
|
||||
"airbnb.fi",
|
||||
"airbnb.fr",
|
||||
"airbnb.gr",
|
||||
"airbnb.gy",
|
||||
"airbnb.hu",
|
||||
"airbnb.ie",
|
||||
"airbnb.is",
|
||||
"airbnb.it",
|
||||
"airbnb.jp",
|
||||
"airbnb.mx",
|
||||
"airbnb.nl",
|
||||
"airbnb.no",
|
||||
"airbnb.pl",
|
||||
"airbnb.pt",
|
||||
"airbnb.ru",
|
||||
"airbnb.se"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 73,
|
||||
"Domains": [
|
||||
"eventbrite.at",
|
||||
"eventbrite.be",
|
||||
"eventbrite.ca",
|
||||
"eventbrite.ch",
|
||||
"eventbrite.cl",
|
||||
"eventbrite.co.id",
|
||||
"eventbrite.co.in",
|
||||
"eventbrite.co.kr",
|
||||
"eventbrite.co.nz",
|
||||
"eventbrite.co.uk",
|
||||
"eventbrite.co.ve",
|
||||
"eventbrite.com",
|
||||
"eventbrite.com.au",
|
||||
"eventbrite.com.bo",
|
||||
"eventbrite.com.br",
|
||||
"eventbrite.com.co",
|
||||
"eventbrite.com.hk",
|
||||
"eventbrite.com.hn",
|
||||
"eventbrite.com.pe",
|
||||
"eventbrite.com.sg",
|
||||
"eventbrite.com.tr",
|
||||
"eventbrite.com.tw",
|
||||
"eventbrite.cz",
|
||||
"eventbrite.de",
|
||||
"eventbrite.dk",
|
||||
"eventbrite.fi",
|
||||
"eventbrite.fr",
|
||||
"eventbrite.gy",
|
||||
"eventbrite.hu",
|
||||
"eventbrite.ie",
|
||||
"eventbrite.is",
|
||||
"eventbrite.it",
|
||||
"eventbrite.jp",
|
||||
"eventbrite.mx",
|
||||
"eventbrite.nl",
|
||||
"eventbrite.no",
|
||||
"eventbrite.pl",
|
||||
"eventbrite.pt",
|
||||
"eventbrite.ru",
|
||||
"eventbrite.se"
|
||||
],
|
||||
"Excluded": false
|
||||
},
|
||||
{
|
||||
"Type": 74,
|
||||
"Domains": [
|
||||
"stackexchange.com",
|
||||
"superuser.com",
|
||||
"stackoverflow.com",
|
||||
"serverfault.com",
|
||||
"mathoverflow.net"
|
||||
],
|
||||
"Excluded": false
|
||||
}
|
||||
]
|
||||
+14
-10
@@ -2,7 +2,7 @@ mod accounts;
|
||||
mod ciphers;
|
||||
mod folders;
|
||||
mod organizations;
|
||||
mod two_factor;
|
||||
pub(crate) mod two_factor;
|
||||
|
||||
use self::accounts::*;
|
||||
use self::ciphers::*;
|
||||
@@ -14,10 +14,12 @@ pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
register,
|
||||
profile,
|
||||
post_profile,
|
||||
get_public_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_sstamp,
|
||||
post_email_token,
|
||||
post_email,
|
||||
delete_account,
|
||||
revision_date,
|
||||
@@ -39,6 +41,7 @@ pub fn routes() -> Vec<Route> {
|
||||
post_cipher,
|
||||
put_cipher,
|
||||
delete_cipher_post,
|
||||
delete_cipher_post_admin,
|
||||
delete_cipher,
|
||||
delete_cipher_selected,
|
||||
delete_all,
|
||||
@@ -55,13 +58,16 @@ pub fn routes() -> Vec<Route> {
|
||||
get_twofactor,
|
||||
get_recover,
|
||||
recover,
|
||||
disable_twofactor,
|
||||
generate_authenticator,
|
||||
activate_authenticator,
|
||||
disable_authenticator,
|
||||
generate_u2f,
|
||||
activate_u2f,
|
||||
|
||||
get_organization,
|
||||
create_organization,
|
||||
delete_organization,
|
||||
leave_organization,
|
||||
get_user_collections,
|
||||
get_org_collections,
|
||||
get_org_collection_detail,
|
||||
@@ -106,8 +112,7 @@ use auth::Headers;
|
||||
|
||||
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
|
||||
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
println!("UUID: {:#?}", uuid);
|
||||
println!("DATA: {:#?}", data);
|
||||
let _data: Value = data.into_inner();
|
||||
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
@@ -125,9 +130,8 @@ fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: D
|
||||
|
||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
println!("UUID: {:#?}", uuid);
|
||||
println!("DATA: {:#?}", data);
|
||||
|
||||
let _data: Value = data.into_inner();
|
||||
|
||||
let device = match Device::find_by_uuid(&uuid, &conn) {
|
||||
Some(device) => device,
|
||||
None => err!("Device not found")
|
||||
@@ -150,7 +154,7 @@ struct GlobalDomain {
|
||||
Excluded: bool,
|
||||
}
|
||||
|
||||
const GLOBAL_DOMAINS: &'static str = include_str!("global_domains.json");
|
||||
const GLOBAL_DOMAINS: &str = include_str!("global_domains.json");
|
||||
|
||||
#[get("/settings/domains")]
|
||||
fn get_eq_domains(headers: Headers) -> JsonResult {
|
||||
@@ -185,8 +189,8 @@ struct EquivDomainData {
|
||||
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
let data: EquivDomainData = data.into_inner().data;
|
||||
|
||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or(Vec::new());
|
||||
let equivalent_domains = data.EquivalentDomains.unwrap_or(Vec::new());
|
||||
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
|
||||
let equivalent_domains = data.EquivalentDomains.unwrap_or_default();
|
||||
|
||||
let mut user = headers.user;
|
||||
use serde_json::to_string;
|
||||
|
||||
@@ -73,6 +73,29 @@ fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers:
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/leave")]
|
||||
fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
|
||||
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
|
||||
None => err!("User not part of organization"),
|
||||
Some(user_org) => {
|
||||
if user_org.type_ == UserOrgType::Owner as i32 {
|
||||
let num_owners = UserOrganization::find_by_org_and_type(
|
||||
&org_id, UserOrgType::Owner as i32, &conn)
|
||||
.len();
|
||||
|
||||
if num_owners <= 1 {
|
||||
err!("The last owner can't leave")
|
||||
}
|
||||
}
|
||||
|
||||
match user_org.delete(&conn) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => err!("Failed leaving the organization")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>")]
|
||||
fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult {
|
||||
match Organization::find_by_uuid(&org_id, &conn) {
|
||||
@@ -288,8 +311,8 @@ fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonRes
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct CollectionData {
|
||||
id: String,
|
||||
readOnly: bool,
|
||||
Id: String,
|
||||
ReadOnly: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -305,7 +328,7 @@ struct InviteData {
|
||||
fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
||||
let data: InviteData = data.into_inner().data;
|
||||
|
||||
let new_type = match UserOrgType::from_str(&data.Type.to_string()) {
|
||||
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
|
||||
Some(new_type) => new_type as i32,
|
||||
None => err!("Invalid type")
|
||||
};
|
||||
@@ -319,9 +342,8 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
match user_opt {
|
||||
None => err!("User email does not exist"),
|
||||
Some(user) => {
|
||||
match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn) {
|
||||
Some(_) => err!("User already in organization"),
|
||||
None => ()
|
||||
if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
|
||||
err!("User already in organization")
|
||||
}
|
||||
|
||||
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||
@@ -331,13 +353,12 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||
|
||||
// If no accessAll, add the collections received
|
||||
if !access_all {
|
||||
for col in data.Collections.iter() {
|
||||
match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn) {
|
||||
for col in &data.Collections {
|
||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||
None => err!("Collection not found in Organization"),
|
||||
Some(collection) => {
|
||||
match CollectionUser::save(&user.uuid, &collection.uuid, col.readOnly, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed saving collection access for user")
|
||||
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
|
||||
err!("Failed saving collection access for user")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,7 +396,7 @@ fn confirm_invite(org_id: String, user_id: String, data: JsonUpcase<Value>, head
|
||||
}
|
||||
|
||||
user_to_confirm.status = UserOrgStatus::Confirmed as i32;
|
||||
user_to_confirm.key = match data["key"].as_str() {
|
||||
user_to_confirm.key = match data["Key"].as_str() {
|
||||
Some(key) => key.to_string(),
|
||||
None => err!("Invalid key provided")
|
||||
};
|
||||
@@ -411,7 +432,7 @@ struct EditUserData {
|
||||
fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
||||
let data: EditUserData = data.into_inner().data;
|
||||
|
||||
let new_type = match UserOrgType::from_str(&data.Type.to_string()) {
|
||||
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
|
||||
Some(new_type) => new_type as i32,
|
||||
None => err!("Invalid type")
|
||||
};
|
||||
@@ -449,21 +470,19 @@ fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, he
|
||||
|
||||
// Delete all the odd collections
|
||||
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
|
||||
match c.delete(&conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed deleting old collection assignment")
|
||||
if c.delete(&conn).is_err() {
|
||||
err!("Failed deleting old collection assignment")
|
||||
}
|
||||
}
|
||||
|
||||
// If no accessAll, add the collections received
|
||||
if !data.AccessAll {
|
||||
for col in data.Collections.iter() {
|
||||
match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn) {
|
||||
for col in &data.Collections {
|
||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||
None => err!("Collection not found in Organization"),
|
||||
Some(collection) => {
|
||||
match CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.readOnly, &conn) {
|
||||
Ok(()) => (),
|
||||
Err(_) => err!("Failed saving collection access for user")
|
||||
if CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
|
||||
err!("Failed saving collection access for user")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+330
-54
File diff suppressed because it is too large
Load Diff
+69
-43
@@ -1,4 +1,3 @@
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::fs::{create_dir_all, File};
|
||||
|
||||
@@ -23,24 +22,59 @@ fn icon(domain: String) -> Content<Vec<u8>> {
|
||||
return Content(icon_type, get_fallback_icon());
|
||||
}
|
||||
|
||||
let url = format!("https://icons.bitwarden.com/{}/icon.png", domain);
|
||||
|
||||
// Get the icon, or fallback in case of error
|
||||
let icon = match get_icon_cached(&domain, &url) {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => return Content(icon_type, get_fallback_icon())
|
||||
};
|
||||
let icon = get_icon(&domain);
|
||||
|
||||
Content(icon_type, icon)
|
||||
}
|
||||
|
||||
fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
|
||||
fn get_icon (domain: &str) -> Vec<u8> {
|
||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder, domain);
|
||||
|
||||
if let Some(icon) = get_cached_icon(&path) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
let url = get_icon_url(&domain);
|
||||
|
||||
// Get the icon, or fallback in case of error
|
||||
match download_icon(&url) {
|
||||
Ok(icon) => {
|
||||
save_icon(&path, &icon);
|
||||
icon
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error downloading icon: {:?}", e);
|
||||
get_fallback_icon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
|
||||
// Try to read the cached icon, and return it if it exists
|
||||
if let Ok(mut f) = File::open(path) {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
if f.read_to_end(&mut buffer).is_ok() {
|
||||
return Some(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_icon_url(domain: &str) -> String {
|
||||
if CONFIG.local_icon_extractor {
|
||||
format!("http://{}/favicon.ico", domain)
|
||||
} else {
|
||||
format!("https://icons.bitwarden.com/{}/icon.png", domain)
|
||||
}
|
||||
}
|
||||
|
||||
fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
|
||||
println!("Downloading icon for {}...", url);
|
||||
let mut res = reqwest::get(url)?;
|
||||
|
||||
res = match res.error_for_status() {
|
||||
Err(e) => return Err(e),
|
||||
Ok(res) => res
|
||||
};
|
||||
res = res.error_for_status()?;
|
||||
|
||||
let mut buffer: Vec<u8> = vec![];
|
||||
res.copy_to(&mut buffer)?;
|
||||
@@ -48,39 +82,31 @@ fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn get_icon_cached(key: &str, url: &str) -> io::Result<Vec<u8>> {
|
||||
create_dir_all(&CONFIG.icon_cache_folder)?;
|
||||
let path = &format!("{}/{}.png", CONFIG.icon_cache_folder, key);
|
||||
fn save_icon(path: &str, icon: &[u8]) {
|
||||
create_dir_all(&CONFIG.icon_cache_folder).expect("Error creating icon cache");
|
||||
|
||||
// Try to read the cached icon, and return it if it exists
|
||||
match File::open(path) {
|
||||
Ok(mut f) => {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
if f.read_to_end(&mut buffer).is_ok() {
|
||||
return Ok(buffer);
|
||||
}
|
||||
/* If error reading file continue */
|
||||
}
|
||||
Err(_) => { /* Continue */ }
|
||||
}
|
||||
|
||||
println!("Downloading icon for {}...", key);
|
||||
let icon = match get_icon(url) {
|
||||
Ok(icon) => icon,
|
||||
Err(_) => return Err(io::Error::new(io::ErrorKind::NotFound, ""))
|
||||
if let Ok(mut f) = File::create(path) {
|
||||
f.write_all(icon).expect("Error writing icon file");
|
||||
};
|
||||
|
||||
// Save the currently downloaded icon
|
||||
match File::create(path) {
|
||||
Ok(mut f) => { f.write_all(&icon).expect("Error writing icon file"); }
|
||||
Err(_) => { /* Continue */ }
|
||||
};
|
||||
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
const FALLBACK_ICON_URL: &str = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
|
||||
|
||||
fn get_fallback_icon() -> Vec<u8> {
|
||||
let fallback_icon = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
|
||||
get_icon_cached("default", fallback_icon).unwrap()
|
||||
let path = format!("{}/default.png", CONFIG.icon_cache_folder);
|
||||
|
||||
if let Some(icon) = get_cached_icon(&path) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
match download_icon(FALLBACK_ICON_URL) {
|
||||
Ok(icon) => {
|
||||
save_icon(&path, &icon);
|
||||
icon
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error downloading fallback icon: {:?}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+172
-117
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -1,4 +1,4 @@
|
||||
mod core;
|
||||
pub(crate) mod core;
|
||||
mod icons;
|
||||
mod identity;
|
||||
mod web;
|
||||
@@ -12,8 +12,9 @@ use rocket::response::status::BadRequest;
|
||||
use rocket_contrib::Json;
|
||||
|
||||
// Type aliases for API methods results
|
||||
type JsonResult = Result<Json, BadRequest<Json>>;
|
||||
type EmptyResult = Result<(), BadRequest<Json>>;
|
||||
type ApiResult<T> = Result<T, BadRequest<Json>>;
|
||||
type JsonResult = ApiResult<Json>;
|
||||
type EmptyResult = ApiResult<()>;
|
||||
|
||||
use util;
|
||||
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
||||
@@ -25,7 +26,7 @@ struct PasswordData {
|
||||
MasterPasswordHash: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
enum NumberOrString {
|
||||
Number(i32),
|
||||
@@ -33,14 +34,14 @@ enum NumberOrString {
|
||||
}
|
||||
|
||||
impl NumberOrString {
|
||||
fn to_string(self) -> String {
|
||||
fn into_string(self) -> String {
|
||||
match self {
|
||||
NumberOrString::Number(n) => n.to_string(),
|
||||
NumberOrString::String(s) => s
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i32(self) -> Option<i32> {
|
||||
fn into_i32(self) -> Option<i32> {
|
||||
match self {
|
||||
NumberOrString::Number(n) => Some(n),
|
||||
NumberOrString::String(s) => s.parse().ok()
|
||||
|
||||
+45
-16
@@ -1,39 +1,68 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, NamedFile, Responder};
|
||||
use rocket::response::content::Content;
|
||||
use rocket::http::ContentType;
|
||||
use rocket::Route;
|
||||
use rocket::response::NamedFile;
|
||||
use rocket_contrib::Json;
|
||||
use rocket_contrib::{Json, Value};
|
||||
|
||||
use CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, files, attachments, alive]
|
||||
if CONFIG.web_vault_enabled {
|
||||
routes![web_index, app_id, web_files, attachments, alive]
|
||||
} else {
|
||||
routes![attachments, alive]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
|
||||
#[get("/")]
|
||||
fn index() -> io::Result<NamedFile> {
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.web_vault_folder)
|
||||
.join("index.html"))
|
||||
fn web_index() -> WebHeaders<io::Result<NamedFile>> {
|
||||
web_files("index.html".into())
|
||||
}
|
||||
|
||||
#[get("/app-id.json")]
|
||||
fn app_id() -> WebHeaders<Content<Json<Value>>> {
|
||||
let content_type = ContentType::new("application", "fido.trusted-apps+json");
|
||||
|
||||
WebHeaders(Content(content_type, Json(json!({
|
||||
"trustedFacets": [
|
||||
{
|
||||
"version": { "major": 1, "minor": 0 },
|
||||
"ids": [
|
||||
&CONFIG.domain,
|
||||
"ios:bundle-id:com.8bit.bitwarden",
|
||||
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
|
||||
}]
|
||||
}))))
|
||||
}
|
||||
|
||||
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
|
||||
fn files(p: PathBuf) -> io::Result<NamedFile> {
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.web_vault_folder)
|
||||
.join(p))
|
||||
fn web_files(p: PathBuf) -> WebHeaders<io::Result<NamedFile>> {
|
||||
WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))
|
||||
}
|
||||
|
||||
struct WebHeaders<R>(R);
|
||||
|
||||
impl<'r, R: Responder<'r>> Responder<'r> for WebHeaders<R> {
|
||||
fn respond_to(self, req: &Request) -> response::Result<'r> {
|
||||
let mut res = self.0.respond_to(req)?;
|
||||
|
||||
res.set_raw_header("Referrer-Policy", "same-origin");
|
||||
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
||||
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
||||
res.set_raw_header("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/attachments/<uuid>/<file..>")]
|
||||
fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
|
||||
NamedFile::open(
|
||||
Path::new(&CONFIG.attachments_folder)
|
||||
.join(uuid)
|
||||
.join(file)
|
||||
)
|
||||
NamedFile::open(Path::new(&CONFIG.attachments_folder).join(uuid).join(file))
|
||||
}
|
||||
|
||||
|
||||
|
||||
+30
-7
@@ -11,10 +11,11 @@ use serde::ser::Serialize;
|
||||
use CONFIG;
|
||||
|
||||
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
|
||||
pub const JWT_ISSUER: &'static str = "localhost:8000/identity";
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
|
||||
pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
|
||||
|
||||
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
|
||||
|
||||
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
|
||||
@@ -30,9 +31,9 @@ lazy_static! {
|
||||
|
||||
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
|
||||
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
|
||||
Ok(token) => return token,
|
||||
Ok(token) => token,
|
||||
Err(e) => panic!("Error encoding jwt {}", e)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
|
||||
@@ -42,7 +43,7 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
|
||||
validate_iat: true,
|
||||
validate_nbf: true,
|
||||
aud: None,
|
||||
iss: Some(JWT_ISSUER.into()),
|
||||
iss: Some(JWT_ISSUER.clone()),
|
||||
sub: None,
|
||||
algorithms: vec![JWT_ALGORITHM],
|
||||
};
|
||||
@@ -109,9 +110,31 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||
let headers = request.headers();
|
||||
|
||||
// Get host
|
||||
let host = match headers.get_one("Host") {
|
||||
Some(host) => format!("http://{}", host), // TODO: Check if HTTPS
|
||||
_ => String::new()
|
||||
let host = if CONFIG.domain_set {
|
||||
CONFIG.domain.clone()
|
||||
} else if let Some(referer) = headers.get_one("Referer") {
|
||||
referer.to_string()
|
||||
} else {
|
||||
// Try to guess from the headers
|
||||
use std::env;
|
||||
|
||||
let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") {
|
||||
proto
|
||||
} else if env::var("ROCKET_TLS").is_ok() {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
|
||||
let host = if let Some(host) = headers.get_one("X-Forwarded-Host") {
|
||||
host
|
||||
} else if let Some(host) = headers.get_one("Host") {
|
||||
host
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
format!("{}://{}", protocol, host)
|
||||
};
|
||||
|
||||
// Get access_token
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType};
|
||||
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType, UserOrgStatus};
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "ciphers"]
|
||||
@@ -84,7 +84,7 @@ impl Cipher {
|
||||
// To remove backwards compatibility, just remove this entire section
|
||||
// and remove the compat code from ciphers::update_cipher_from_data
|
||||
if self.type_ == 1 && data_json["Uris"].is_array() {
|
||||
let uri = data_json["Uris"][0]["uri"].clone();
|
||||
let uri = data_json["Uris"][0]["Uri"].clone();
|
||||
data_json["Uri"] = uri;
|
||||
}
|
||||
// TODO: ******* Backwards compat end **********
|
||||
@@ -97,7 +97,7 @@ impl Cipher {
|
||||
"Favorite": self.favorite,
|
||||
"OrganizationId": self.organization_uuid,
|
||||
"Attachments": attachments_json,
|
||||
"OrganizationUseTotp": false,
|
||||
"OrganizationUseTotp": true,
|
||||
"CollectionIds": self.get_collections(user_uuid, &conn),
|
||||
|
||||
"Name": self.name,
|
||||
@@ -266,7 +266,9 @@ impl Cipher {
|
||||
ciphers::table
|
||||
.left_join(users_organizations::table.on(
|
||||
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
users_organizations::user_uuid.eq(user_uuid).and(
|
||||
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||
)
|
||||
)
|
||||
))
|
||||
.left_join(ciphers_collections::table)
|
||||
|
||||
@@ -43,11 +43,14 @@ impl Device {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_twofactor_remember(&mut self) {
|
||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||
use data_encoding::BASE64;
|
||||
use crypto;
|
||||
|
||||
self.twofactor_remember = Some(BASE64.encode(&crypto::get_random(vec![0u8; 180])));
|
||||
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
|
||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||
|
||||
twofactor_remember
|
||||
}
|
||||
|
||||
pub fn delete_twofactor_remember(&mut self) {
|
||||
|
||||
@@ -6,6 +6,7 @@ mod user;
|
||||
|
||||
mod collection;
|
||||
mod organization;
|
||||
mod two_factor;
|
||||
|
||||
pub use self::attachment::Attachment;
|
||||
pub use self::cipher::Cipher;
|
||||
@@ -15,3 +16,4 @@ pub use self::user::User;
|
||||
pub use self::organization::Organization;
|
||||
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
|
||||
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
|
||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||
@@ -66,11 +66,11 @@ impl Organization {
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
|
||||
"Use2fa": false,
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false,
|
||||
"UseEvents": false,
|
||||
"UseGroups": false,
|
||||
"UseTotp": false,
|
||||
"UseTotp": true,
|
||||
|
||||
"BusinessName": null,
|
||||
"BusinessAddress1": null,
|
||||
@@ -80,8 +80,8 @@ impl Organization {
|
||||
"BusinessTaxNumber": null,
|
||||
|
||||
"BillingEmail": self.billing_email,
|
||||
"Plan": "Free",
|
||||
"PlanType": 0, // Free plan
|
||||
"Plan": "TeamsAnnually",
|
||||
"PlanType": 5, // TeamsAnnually plan
|
||||
|
||||
"Object": "organization",
|
||||
})
|
||||
@@ -153,11 +153,11 @@ impl UserOrganization {
|
||||
"Seats": 10,
|
||||
"MaxCollections": 10,
|
||||
|
||||
"Use2fa": false,
|
||||
"Use2fa": true,
|
||||
"UseDirectory": false,
|
||||
"UseEvents": false,
|
||||
"UseGroups": false,
|
||||
"UseTotp": false,
|
||||
"UseTotp": true,
|
||||
|
||||
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::User;
|
||||
|
||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
||||
#[table_name = "twofactor"]
|
||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||
#[primary_key(uuid)]
|
||||
pub struct TwoFactor {
|
||||
pub uuid: String,
|
||||
pub user_uuid: String,
|
||||
pub type_: i32,
|
||||
pub enabled: bool,
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromPrimitive, ToPrimitive)]
|
||||
pub enum TwoFactorType {
|
||||
Authenticator = 0,
|
||||
Email = 1,
|
||||
Duo = 2,
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6,
|
||||
|
||||
// These are implementation details
|
||||
U2fRegisterChallenge = 1000,
|
||||
U2fLoginChallenge = 1001,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl TwoFactor {
|
||||
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
user_uuid,
|
||||
type_: type_ as i32,
|
||||
enabled: true,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
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) -> JsonValue {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Key": "", // This key and value vary
|
||||
"Object": "twoFactorAuthenticator" // This value varies
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_json_list(&self) -> JsonValue {
|
||||
json!({
|
||||
"Enabled": self.enabled,
|
||||
"Type": self.type_,
|
||||
"Object": "twoFactorProvider"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use db::DbConn;
|
||||
use db::schema::twofactor;
|
||||
|
||||
/// Database methods
|
||||
impl TwoFactor {
|
||||
pub fn save(&self, conn: &DbConn) -> QueryResult<usize> {
|
||||
diesel::replace_into(twofactor::table)
|
||||
.values(self)
|
||||
.execute(&**conn)
|
||||
}
|
||||
|
||||
pub fn delete(self, conn: &DbConn) -> QueryResult<usize> {
|
||||
diesel::delete(
|
||||
twofactor::table.filter(
|
||||
twofactor::uuid.eq(self.uuid)
|
||||
)
|
||||
).execute(&**conn)
|
||||
}
|
||||
|
||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
.load::<Self>(&**conn).expect("Error loading twofactor")
|
||||
}
|
||||
|
||||
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> {
|
||||
twofactor::table
|
||||
.filter(twofactor::user_uuid.eq(user_uuid))
|
||||
.filter(twofactor::type_.eq(type_))
|
||||
.first::<Self>(&**conn).ok()
|
||||
}
|
||||
}
|
||||
+7
-25
@@ -27,7 +27,8 @@ pub struct User {
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
|
||||
pub totp_secret: Option<String>,
|
||||
#[column_name = "totp_secret"]
|
||||
_totp_secret: Option<String>,
|
||||
pub totp_recover: Option<String>,
|
||||
|
||||
pub security_stamp: String,
|
||||
@@ -64,7 +65,7 @@ impl User {
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
||||
totp_secret: None,
|
||||
_totp_secret: None,
|
||||
totp_recover: None,
|
||||
|
||||
equivalent_domains: "[]".to_string(),
|
||||
@@ -97,28 +98,6 @@ impl User {
|
||||
pub fn reset_security_stamp(&mut self) {
|
||||
self.security_stamp = Uuid::new_v4().to_string();
|
||||
}
|
||||
|
||||
pub fn requires_twofactor(&self) -> bool {
|
||||
self.totp_secret.is_some()
|
||||
}
|
||||
|
||||
pub fn check_totp_code(&self, totp_code: u64) -> bool {
|
||||
if let Some(ref totp_secret) = self.totp_secret {
|
||||
// Validate totp
|
||||
use data_encoding::BASE32;
|
||||
use oath::{totp_raw_now, HashType};
|
||||
|
||||
let decoded_secret = match BASE32.decode(totp_secret.as_bytes()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false
|
||||
};
|
||||
|
||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
||||
generated == totp_code
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use diesel;
|
||||
@@ -130,10 +109,13 @@ use db::schema::users;
|
||||
impl User {
|
||||
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
|
||||
use super::UserOrganization;
|
||||
use super::TwoFactor;
|
||||
|
||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
|
||||
let twofactor_enabled = TwoFactor::find_by_user(&self.uuid, conn).len() > 0;
|
||||
|
||||
json!({
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
@@ -142,7 +124,7 @@ impl User {
|
||||
"Premium": true,
|
||||
"MasterPasswordHint": self.password_hint,
|
||||
"Culture": "en-US",
|
||||
"TwoFactorEnabled": self.totp_secret.is_some(),
|
||||
"TwoFactorEnabled": twofactor_enabled,
|
||||
"Key": self.key,
|
||||
"PrivateKey": self.private_key,
|
||||
"SecurityStamp": self.security_stamp,
|
||||
|
||||
@@ -79,6 +79,17 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
twofactor (uuid) {
|
||||
uuid -> Text,
|
||||
user_uuid -> Text,
|
||||
#[sql_name = "type"]
|
||||
type_ -> Integer,
|
||||
enabled -> Bool,
|
||||
data -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (uuid) {
|
||||
uuid -> Text,
|
||||
@@ -132,6 +143,7 @@ joinable!(devices -> users (user_uuid));
|
||||
joinable!(folders -> users (user_uuid));
|
||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||
joinable!(twofactor -> users (user_uuid));
|
||||
joinable!(users_collections -> collections (collection_uuid));
|
||||
joinable!(users_collections -> users (user_uuid));
|
||||
joinable!(users_organizations -> organizations (org_uuid));
|
||||
@@ -146,6 +158,7 @@ allow_tables_to_appear_in_same_query!(
|
||||
folders,
|
||||
folders_ciphers,
|
||||
organizations,
|
||||
twofactor,
|
||||
users,
|
||||
users_collections,
|
||||
users_organizations,
|
||||
|
||||
+38
-11
@@ -19,11 +19,15 @@ extern crate chrono;
|
||||
extern crate oath;
|
||||
extern crate data_encoding;
|
||||
extern crate jsonwebtoken as jwt;
|
||||
extern crate u2f;
|
||||
extern crate dotenv;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
extern crate num_traits;
|
||||
|
||||
use std::{io, env, path::Path, process::{exit, Command}};
|
||||
use std::{env, path::Path, process::{exit, Command}};
|
||||
use rocket::Rocket;
|
||||
|
||||
#[macro_use]
|
||||
@@ -46,16 +50,25 @@ fn init_rocket() -> Rocket {
|
||||
// Embed the migrations from the migrations folder into the application
|
||||
// This way, the program automatically migrates the database to the latest version
|
||||
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
|
||||
embed_migrations!();
|
||||
#[allow(unused_imports)]
|
||||
mod migrations {
|
||||
embed_migrations!();
|
||||
|
||||
pub fn run_migrations() {
|
||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||
let connection = ::db::get_connection().expect("Can't conect to DB");
|
||||
|
||||
use std::io::stdout;
|
||||
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
check_db();
|
||||
check_rsa_keys();
|
||||
check_web_vault();
|
||||
check_web_vault();
|
||||
migrations::run_migrations();
|
||||
|
||||
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
|
||||
let connection = db::get_connection().expect("Can't conect to DB");
|
||||
embedded_migrations::run_with_output(&connection, &mut io::stdout()).expect("Can't run migrations");
|
||||
|
||||
init_rocket().launch();
|
||||
}
|
||||
@@ -118,6 +131,10 @@ fn check_rsa_keys() {
|
||||
}
|
||||
|
||||
fn check_web_vault() {
|
||||
if !CONFIG.web_vault_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
|
||||
|
||||
if !index_path.exists() {
|
||||
@@ -142,9 +159,13 @@ pub struct Config {
|
||||
public_rsa_key: String,
|
||||
|
||||
web_vault_folder: String,
|
||||
web_vault_enabled: bool,
|
||||
|
||||
local_icon_extractor: bool,
|
||||
signups_allowed: bool,
|
||||
password_iterations: i32,
|
||||
domain: String,
|
||||
domain_set: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -152,21 +173,27 @@ impl Config {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let df = env::var("DATA_FOLDER").unwrap_or("data".into());
|
||||
let key = env::var("RSA_KEY_NAME").unwrap_or("rsa_key".into());
|
||||
let key = env::var("RSA_KEY_FILENAME").unwrap_or(format!("{}/{}", &df, "rsa_key"));
|
||||
|
||||
let domain = env::var("DOMAIN");
|
||||
|
||||
Config {
|
||||
database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
|
||||
icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
|
||||
attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
|
||||
|
||||
private_rsa_key: format!("{}/{}.der", &df, &key),
|
||||
private_rsa_key_pem: format!("{}/{}.pem", &df, &key),
|
||||
public_rsa_key: format!("{}/{}.pub.der", &df, &key),
|
||||
private_rsa_key: format!("{}.der", &key),
|
||||
private_rsa_key_pem: format!("{}.pem", &key),
|
||||
public_rsa_key: format!("{}.pub.der", &key),
|
||||
|
||||
web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
|
||||
web_vault_enabled: util::parse_option_string(env::var("WEB_VAULT_ENABLED").ok()).unwrap_or(true),
|
||||
|
||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(false),
|
||||
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
|
||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
|
||||
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
|
||||
domain_set: domain.is_ok(),
|
||||
domain: domain.unwrap_or("http://localhost".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user