mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-11 03:05:58 +03:00
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fd27759a95 | ||
|
01d8056c73 | ||
|
81fa33ebb5 | ||
|
e8aa3bc066 | ||
|
0bf0125e82 | ||
|
6209e778e5 | ||
|
5323283f98 | ||
|
57e17d0648 | ||
|
da55d5ec70 | ||
|
828a060698 | ||
|
3e5971b9db | ||
|
47c2625d38 | ||
|
49af9cf4f5 | ||
|
6b1daeba05 | ||
|
9f1240d8d9 | ||
|
a8138be69b | ||
|
ea57dc3bc9 | ||
|
131348a49f | ||
|
b22564cb00 | ||
|
16eb0a56f9 | ||
|
3e4ff47a38 | ||
|
8ea01a67f6 | ||
|
aa5cc642e1 | ||
|
a121cb6f00 | ||
|
60164182ae | ||
|
f842a80cdb | ||
|
4b6a574ee0 | ||
|
f9ebb780f9 | ||
|
1fc6c30652 | ||
|
46a1a013cd | ||
|
551810c486 | ||
|
b987ba506d | ||
|
84810f2bb2 | ||
|
424d666a50 | ||
|
a71359f647 | ||
|
d93c344176 | ||
|
b9c3213b90 | ||
|
95e24ffc51 | ||
|
00d56d7295 | ||
|
7436b454db | ||
|
8da5b99482 | ||
|
2969e87b52 | ||
|
ce62e898c3 | ||
|
431462d839 | ||
|
7d0e234b34 | ||
|
dad1b1bee9 | ||
|
9312cebee3 | ||
|
cdf5b6ec2d | ||
|
ce99fc8f95 | ||
|
a75d050001 | ||
|
75cfd10f11 | ||
|
9859ba6339 | ||
|
513056f711 | ||
|
ebe334fcc7 | ||
|
0eec12472e | ||
|
39106d440a | ||
|
9117095764 | ||
|
099bba950c | ||
|
e37ff60617 | ||
|
5b14608041 | ||
|
ad92692bab | ||
|
d956d42903 | ||
|
d69be7d03a | ||
|
f82de8d00d | ||
|
c836f88ff2 | ||
|
8b660ae090 |
@@ -28,6 +28,7 @@
|
|||||||
# RSA_KEY_FILENAME=data/rsa_key
|
# RSA_KEY_FILENAME=data/rsa_key
|
||||||
# ICON_CACHE_FOLDER=data/icon_cache
|
# ICON_CACHE_FOLDER=data/icon_cache
|
||||||
# ATTACHMENTS_FOLDER=data/attachments
|
# ATTACHMENTS_FOLDER=data/attachments
|
||||||
|
# SENDS_FOLDER=data/sends
|
||||||
|
|
||||||
## Templates data folder, by default uses embedded templates
|
## Templates data folder, by default uses embedded templates
|
||||||
## Check source code to see the format
|
## Check source code to see the format
|
||||||
|
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ignore vendored scripts in GitHub stats
|
||||||
|
src/static/* linguist-vendored
|
||||||
|
|
59
.github/ISSUE_TEMPLATE/bug_report.md
vendored
59
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,51 +1,66 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Use this ONLY for bugs in bitwarden_rs itself. Use the Discourse forum (link below) to request features or get help with usage/configuration. If in doubt, use the forum.
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
# ###
|
# ###
|
||||||
NOTE: Please update to the latest version of bitwarden_rs before reporting an issue!
|
NOTE: Please update to the latest version of bitwarden_rs before reporting an issue!
|
||||||
This saves you and us a lot of time and troubleshooting.
|
This saves you and us a lot of time and troubleshooting.
|
||||||
See: https://github.com/dani-garcia/bitwarden_rs/issues/1180
|
See:
|
||||||
# ###
|
* https://github.com/dani-garcia/bitwarden_rs/issues/1180
|
||||||
|
* https://github.com/dani-garcia/bitwarden_rs/wiki/Updating-the-bitwarden-image
|
||||||
|
# ###
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please fill out the following template to make solving your problem easier and faster for us.
|
Please fill out the following template to make solving your problem easier and faster for us.
|
||||||
This is only a guideline. If you think that parts are unnecessary for your issue, feel free to remove them.
|
This is only a guideline. If you think that parts are unnecessary for your issue, feel free to remove them.
|
||||||
|
|
||||||
Remember to hide/obfuscate personal and confidential information,
|
Remember to hide/redact personal or confidential information,
|
||||||
such as names, global IP/DNS addresses and especially passwords, if necessary.
|
such as passwords, IP addresses, and DNS names as appropriate.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Subject of the issue
|
### Subject of the issue
|
||||||
<!-- Describe your issue here.-->
|
<!-- Describe your issue here. -->
|
||||||
|
|
||||||
### Your environment
|
### Deployment environment
|
||||||
<!-- The version number, obtained from the logs or the admin diagnostics page -->
|
|
||||||
<!-- Remember to check your issue on the latest version first! -->
|
<!--
|
||||||
* Bitwarden_rs version:
|
=========================================================================================
|
||||||
<!-- How the server was installed: Docker image / package / built from source -->
|
Preferably, use the `Generate Support String` button on the admin page's Diagnostics tab.
|
||||||
|
That will auto-generate most of the info requested in this section.
|
||||||
|
=========================================================================================
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- The version number, obtained from the logs (at startup) or the admin diagnostics page -->
|
||||||
|
<!-- This is NOT the version number shown on the web vault, which is versioned separately from bitwarden_rs -->
|
||||||
|
<!-- Remember to check if your issue exists on the latest version first! -->
|
||||||
|
* bitwarden_rs version:
|
||||||
|
|
||||||
|
<!-- How the server was installed: Docker image, OS package, built from source, etc. -->
|
||||||
* Install method:
|
* Install method:
|
||||||
* Clients used: <!-- if applicable -->
|
|
||||||
|
* Clients used: <!-- web vault, desktop, Android, iOS, etc. (if applicable) -->
|
||||||
|
|
||||||
* Reverse proxy and version: <!-- if applicable -->
|
* Reverse proxy and version: <!-- if applicable -->
|
||||||
* Version of mysql/postgresql: <!-- if applicable -->
|
|
||||||
* Other relevant information:
|
* MySQL/MariaDB or PostgreSQL version: <!-- if applicable -->
|
||||||
|
|
||||||
|
* Other relevant details:
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to reproduce
|
||||||
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
|
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
|
||||||
and how did you start bitwarden_rs? -->
|
and how did you start bitwarden_rs? -->
|
||||||
|
|
||||||
### Expected behaviour
|
### Expected behaviour
|
||||||
<!-- Tell us what should happen -->
|
<!-- Tell us what you expected to happen -->
|
||||||
|
|
||||||
### Actual behaviour
|
### Actual behaviour
|
||||||
<!-- Tell us what happens instead -->
|
<!-- Tell us what actually happened -->
|
||||||
|
|
||||||
### Relevant logs
|
### Troubleshooting data
|
||||||
<!-- Share some logfiles, screenshots or output of relevant programs with us. -->
|
<!-- Share any log files, screenshots, or other relevant troubleshooting data -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discourse forum for bitwarden_rs
|
||||||
|
url: https://bitwardenrs.discourse.group/
|
||||||
|
about: Use this forum to request features or get help with usage/configuration.
|
||||||
|
- name: GitHub Discussions for bitwarden_rs
|
||||||
|
url: https://github.com/dani-garcia/bitwarden_rs/discussions
|
||||||
|
about: An alternative to the Discourse forum, if this is easier for you.
|
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your feature requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/feature-requests
|
|
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Help with installation/configuration
|
|
||||||
about: Any questions about the setup of bitwarden_rs
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your third party help requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/help
|
|
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Help with proxy/database/NAS setup
|
|
||||||
about: Any questions about third party software
|
|
||||||
title: ''
|
|
||||||
labels: better for forum
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Please submit all your third party help requests to the forum
|
|
||||||
Link: https://bitwardenrs.discourse.group/c/third-party-help
|
|
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -10,6 +10,15 @@ on:
|
|||||||
- "docker/**"
|
- "docker/**"
|
||||||
- "hooks/**"
|
- "hooks/**"
|
||||||
- "tools/**"
|
- "tools/**"
|
||||||
|
pull_request:
|
||||||
|
# Ignore when there are only changes done too one of these paths
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- "azure-pipelines.yml"
|
||||||
|
- "docker/**"
|
||||||
|
- "hooks/**"
|
||||||
|
- "tools/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -82,10 +91,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
target: ${{ matrix.target-triple }}
|
target: ${{ matrix.target-triple }}
|
||||||
|
components: clippy
|
||||||
# End Uses the rust-toolchain file to determine version
|
# End Uses the rust-toolchain file to determine version
|
||||||
|
|
||||||
|
|
||||||
# Run cargo tests (In release mode to speed up cargo build afterwards)
|
# Run cargo tests (In release mode to speed up future builds)
|
||||||
- name: '`cargo test --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
- name: '`cargo test --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@@ -94,6 +104,15 @@ jobs:
|
|||||||
# End Run cargo tests
|
# End Run cargo tests
|
||||||
|
|
||||||
|
|
||||||
|
# Run cargo clippy (In release mode to speed up future builds)
|
||||||
|
- name: '`cargo clippy --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}
|
||||||
|
# End Run cargo clippy
|
||||||
|
|
||||||
|
|
||||||
# Build the binary
|
# Build the binary
|
||||||
- name: '`cargo build --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
- name: '`cargo build --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
803
Cargo.lock
generated
803
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -32,7 +32,7 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
|
|||||||
rocket_contrib = "0.5.0-dev"
|
rocket_contrib = "0.5.0-dev"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = { version = "0.11.0", features = ["blocking", "json"] }
|
reqwest = { version = "0.11.2", features = ["blocking", "json"] }
|
||||||
|
|
||||||
# multipart/form-data support
|
# multipart/form-data support
|
||||||
multipart = { version = "0.17.1", features = ["server"], default-features = false }
|
multipart = { version = "0.17.1", features = ["server"], default-features = false }
|
||||||
@@ -47,19 +47,19 @@ rmpv = "0.4.7"
|
|||||||
chashmap = "2.2.2"
|
chashmap = "2.2.2"
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.123", features = ["derive"] }
|
serde = { version = "1.0.125", features = ["derive"] }
|
||||||
serde_json = "1.0.62"
|
serde_json = "1.0.64"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
fern = { version = "0.6.0", features = ["syslog-4"] }
|
fern = { version = "0.6.0", features = ["syslog-4"] }
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "1.4.5", features = [ "chrono", "r2d2"] }
|
diesel = { version = "1.4.6", features = [ "chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
|
||||||
# Bundled SQLite
|
# Bundled SQLite
|
||||||
libsqlite3-sys = { version = "0.18.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.20.1", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
@@ -69,9 +69,9 @@ ring = "0.16.20"
|
|||||||
uuid = { version = "0.8.2", features = ["v4"] }
|
uuid = { version = "0.8.2", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = "0.4.19"
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
chrono-tz = "0.5.3"
|
chrono-tz = "0.5.3"
|
||||||
time = "0.2.25"
|
time = "0.2.26"
|
||||||
|
|
||||||
# TOTP library
|
# TOTP library
|
||||||
oath = "0.10.2"
|
oath = "0.10.2"
|
||||||
@@ -86,46 +86,47 @@ jsonwebtoken = "7.2.0"
|
|||||||
u2f = "0.2.0"
|
u2f = "0.2.0"
|
||||||
|
|
||||||
# Yubico Library
|
# Yubico Library
|
||||||
yubico = { version = "0.9.2", features = ["online-tokio"], default-features = false }
|
yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false }
|
||||||
|
|
||||||
# A `dotenv` implementation for Rust
|
# A `dotenv` implementation for Rust
|
||||||
dotenv = { version = "0.15.0", default-features = false }
|
dotenv = { version = "0.15.0", default-features = false }
|
||||||
|
|
||||||
# Lazy initialization
|
# Lazy initialization
|
||||||
once_cell = "1.5.2"
|
once_cell = "1.7.2"
|
||||||
|
|
||||||
# Numerical libraries
|
# Numerical libraries
|
||||||
num-traits = "0.2.14"
|
num-traits = "0.2.14"
|
||||||
num-derive = "0.3.3"
|
num-derive = "0.3.3"
|
||||||
|
|
||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.10.0-alpha.5", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
lettre = { version = "0.10.0-beta.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
|
||||||
newline-converter = "0.1.0"
|
newline-converter = "0.2.0"
|
||||||
|
|
||||||
# Template library
|
# Template library
|
||||||
handlebars = { version = "3.5.2", features = ["dir_source"] }
|
handlebars = { version = "3.5.3", features = ["dir_source"] }
|
||||||
|
|
||||||
# For favicon extraction from main website
|
# For favicon extraction from main website
|
||||||
soup = "0.5.0"
|
html5ever = "0.25.1"
|
||||||
regex = { version = "1.4.3", features = ["std", "perf"], default-features = false }
|
markup5ever_rcdom = "0.1.0"
|
||||||
|
regex = { version = "1.4.5", features = ["std", "perf"], default-features = false }
|
||||||
data-url = "0.1.0"
|
data-url = "0.1.0"
|
||||||
|
|
||||||
# Used by U2F, JWT and Postgres
|
# Used by U2F, JWT and Postgres
|
||||||
openssl = "0.10.32"
|
openssl = "0.10.33"
|
||||||
|
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
# Punycode conversion
|
# Punycode conversion
|
||||||
idna = "0.2.1"
|
idna = "0.2.2"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
structopt = "0.3.21"
|
pico-args = "0.4.0"
|
||||||
|
|
||||||
# Logging panics to logfile instead stderr only
|
# Logging panics to logfile instead stderr only
|
||||||
backtrace = "0.3.56"
|
backtrace = "0.3.56"
|
||||||
|
|
||||||
# Macro ident concatenation
|
# Macro ident concatenation
|
||||||
paste = "1.0.4"
|
paste = "1.0.5"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Use newest ring
|
# Use newest ring
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
|
||||||
|
|
||||||
{% set build_stage_base_image = "rust:1.48" %}
|
{% set build_stage_base_image = "rust:1.50" %}
|
||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
{% if "amd64" in target_file %}
|
{% if "amd64" in target_file %}
|
||||||
{% set build_stage_base_image = "clux/muslrust:nightly-2021-01-25" %}
|
{% set build_stage_base_image = "clux/muslrust:nightly-2021-02-22" %}
|
||||||
{% set runtime_stage_base_image = "alpine:3.12" %}
|
{% set runtime_stage_base_image = "alpine:3.13" %}
|
||||||
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
|
||||||
{% elif "armv7" in target_file %}
|
{% elif "armv7" in target_file %}
|
||||||
{% set build_stage_base_image = "messense/rust-musl-cross:armv7-musleabihf" %}
|
{% set build_stage_base_image = "messense/rust-musl-cross:armv7-musleabihf" %}
|
||||||
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.12" %}
|
{% set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.13" %}
|
||||||
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
{% set package_arch_target = "armv7-unknown-linux-musleabihf" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif "amd64" in target_file %}
|
{% elif "amd64" in target_file %}
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
# https://docs.docker.com/develop/develop-images/multistage-build/
|
# https://docs.docker.com/develop/develop-images/multistage-build/
|
||||||
# https://whitfin.io/speeding-up-rust-docker-builds/
|
# https://whitfin.io/speeding-up-rust-docker-builds/
|
||||||
####################### VAULT BUILD IMAGE #######################
|
####################### VAULT BUILD IMAGE #######################
|
||||||
{% set vault_version = "2.18.1b" %}
|
{% set vault_version = "2.19.0" %}
|
||||||
{% set vault_image_digest = "sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb" %}
|
{% set vault_image_digest = "sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4" %}
|
||||||
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
# The web-vault digest specifies a particular web-vault build on Docker Hub.
|
||||||
# Using the digest instead of the tag name provides better security,
|
# Using the digest instead of the tag name provides better security,
|
||||||
# as the digest of an image is immutable, whereas a tag name can later
|
# as the digest of an image is immutable, whereas a tag name can later
|
||||||
@@ -93,6 +93,9 @@ RUN rustup set profile minimal
|
|||||||
{% if "alpine" in target_file %}
|
{% if "alpine" in target_file %}
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
|
{% if "armv7" in target_file %}
|
||||||
|
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
||||||
|
{% endif %}
|
||||||
{% elif "arm" in target_file %}
|
{% elif "arm" in target_file %}
|
||||||
# Install required build libs for {{ package_arch_name }} architecture.
|
# Install required build libs for {{ package_arch_name }} architecture.
|
||||||
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
# To compile both mysql and postgresql we need some extra packages for both host arch and target arch
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
OBJECTS := $(shell find -mindepth 2 -name 'Dockerfile*')
|
OBJECTS := $(shell find ./ -mindepth 2 -name 'Dockerfile*')
|
||||||
|
|
||||||
all: $(OBJECTS)
|
all: $(OBJECTS)
|
||||||
|
|
||||||
|
@@ -14,18 +14,18 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.48 as build
|
FROM rust:1.50 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
@@ -14,18 +14,18 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM clux/muslrust:nightly-2021-01-25 as build
|
FROM clux/muslrust:nightly-2021-02-22 as build
|
||||||
|
|
||||||
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
# Alpine-based AMD64 (musl) does not support mysql/mariadb during compile time.
|
||||||
ARG DB=sqlite,postgresql
|
ARG DB=sqlite,postgresql
|
||||||
@@ -70,7 +70,7 @@ RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM alpine:3.12
|
FROM alpine:3.13
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
|
@@ -14,18 +14,18 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.48 as build
|
FROM rust:1.50 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
@@ -14,18 +14,18 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.48 as build
|
FROM rust:1.50 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
@@ -14,18 +14,18 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM rust:1.48 as build
|
FROM rust:1.50 as build
|
||||||
|
|
||||||
# Debian-based builds support multidb
|
# Debian-based builds support multidb
|
||||||
ARG DB=sqlite,mysql,postgresql
|
ARG DB=sqlite,mysql,postgresql
|
||||||
|
@@ -14,15 +14,15 @@
|
|||||||
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
# - From https://hub.docker.com/r/bitwardenrs/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull bitwardenrs/web-vault:v2.18.1b
|
# $ docker pull bitwardenrs/web-vault:v2.19.0
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
|
||||||
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
|
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
|
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
|
||||||
# [bitwardenrs/web-vault:v2.18.1b]
|
# [bitwardenrs/web-vault:v2.19.0]
|
||||||
#
|
#
|
||||||
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
|
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
FROM messense/rust-musl-cross:armv7-musleabihf as build
|
||||||
@@ -38,6 +38,7 @@ RUN rustup set profile minimal
|
|||||||
|
|
||||||
ENV USER "root"
|
ENV USER "root"
|
||||||
ENV RUSTFLAGS='-C link-arg=-s'
|
ENV RUSTFLAGS='-C link-arg=-s'
|
||||||
|
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
|
||||||
|
|
||||||
# Creates a dummy project used to grab dependencies
|
# Creates a dummy project used to grab dependencies
|
||||||
RUN USER=root cargo new --bin /app
|
RUN USER=root cargo new --bin /app
|
||||||
@@ -71,7 +72,7 @@ RUN musl-strip target/armv7-unknown-linux-musleabihf/release/bitwarden_rs
|
|||||||
######################## RUNTIME IMAGE ########################
|
######################## RUNTIME IMAGE ########################
|
||||||
# Create a new stage with a minimal image
|
# Create a new stage with a minimal image
|
||||||
# because we already have a binary built
|
# because we already have a binary built
|
||||||
FROM balenalib/armv7hf-alpine:3.12
|
FROM balenalib/armv7hf-alpine:3.13
|
||||||
|
|
||||||
ENV ROCKET_ENV "staging"
|
ENV ROCKET_ENV "staging"
|
||||||
ENV ROCKET_PORT=80
|
ENV ROCKET_PORT=80
|
||||||
|
1
migrations/mysql/2021-03-11-190243_add_sends/down.sql
Normal file
1
migrations/mysql/2021-03-11-190243_add_sends/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
25
migrations/mysql/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/mysql/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) REFERENCES users (uuid),
|
||||||
|
organization_uuid CHAR(36) REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
akey TEXT NOT NULL,
|
||||||
|
password_hash BLOB,
|
||||||
|
password_salt BLOB,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
expiration_date DATETIME,
|
||||||
|
deletion_date DATETIME NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
25
migrations/postgresql/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/postgresql/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid CHAR(36) REFERENCES users (uuid),
|
||||||
|
organization_uuid CHAR(36) REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
password_hash BYTEA,
|
||||||
|
password_salt BYTEA,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date TIMESTAMP NOT NULL,
|
||||||
|
revision_date TIMESTAMP NOT NULL,
|
||||||
|
expiration_date TIMESTAMP,
|
||||||
|
deletion_date TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sends RENAME COLUMN key TO akey;
|
1
migrations/sqlite/2021-03-11-190243_add_sends/down.sql
Normal file
1
migrations/sqlite/2021-03-11-190243_add_sends/down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE sends;
|
25
migrations/sqlite/2021-03-11-190243_add_sends/up.sql
Normal file
25
migrations/sqlite/2021-03-11-190243_add_sends/up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CREATE TABLE sends (
|
||||||
|
uuid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
user_uuid TEXT REFERENCES users (uuid),
|
||||||
|
organization_uuid TEXT REFERENCES organizations (uuid),
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
atype INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
password_hash BLOB,
|
||||||
|
password_salt BLOB,
|
||||||
|
password_iter INTEGER,
|
||||||
|
|
||||||
|
max_access_count INTEGER,
|
||||||
|
access_count INTEGER NOT NULL,
|
||||||
|
|
||||||
|
creation_date DATETIME NOT NULL,
|
||||||
|
revision_date DATETIME NOT NULL,
|
||||||
|
expiration_date DATETIME,
|
||||||
|
deletion_date DATETIME NOT NULL,
|
||||||
|
|
||||||
|
disabled BOOLEAN NOT NULL
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sends RENAME COLUMN key TO akey;
|
@@ -1 +1 @@
|
|||||||
nightly-2021-01-25
|
nightly-2021-02-22
|
@@ -13,13 +13,13 @@ use rocket::{
|
|||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
|
api::{ApiResult, EmptyResult, NumberOrString},
|
||||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
mail,
|
mail,
|
||||||
util::{format_naive_datetime_local, get_display_size},
|
util::{format_naive_datetime_local, get_display_size, is_running_in_docker},
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,6 +96,27 @@ impl<'a, 'r> FromRequest<'a, 'r> for Referer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct IpHeader(Option<String>);
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for IpHeader {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
if req.headers().get_one(&CONFIG.ip_header()).is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(CONFIG.ip_header())))
|
||||||
|
} else if req.headers().get_one("X-Client-IP").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Client-IP"))))
|
||||||
|
} else if req.headers().get_one("X-Real-IP").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Real-IP"))))
|
||||||
|
} else if req.headers().get_one("X-Forwarded-For").is_some() {
|
||||||
|
Outcome::Success(IpHeader(Some(String::from("X-Forwarded-For"))))
|
||||||
|
} else {
|
||||||
|
Outcome::Success(IpHeader(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Used for `Location` response headers, which must specify an absolute URI
|
/// Used for `Location` response headers, which must specify an absolute URI
|
||||||
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
|
||||||
fn admin_url(referer: Referer) -> String {
|
fn admin_url(referer: Referer) -> String {
|
||||||
@@ -291,17 +312,17 @@ fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
fn logout(mut cookies: Cookies, referer: Referer) -> Result<Redirect, ()> {
|
fn logout(mut cookies: Cookies, referer: Referer) -> Redirect {
|
||||||
cookies.remove(Cookie::named(COOKIE_NAME));
|
cookies.remove(Cookie::named(COOKIE_NAME));
|
||||||
Ok(Redirect::to(admin_url(referer)))
|
Redirect::to(admin_url(referer))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users")]
|
#[get("/users")]
|
||||||
fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult {
|
fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {
|
||||||
let users = User::get_all(&conn);
|
let users = User::get_all(&conn);
|
||||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(Value::Array(users_json)))
|
Json(Value::Array(users_json))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/users/overview")]
|
#[get("/users/overview")]
|
||||||
@@ -475,7 +496,7 @@ fn has_http_access() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/diagnostics")]
|
#[get("/diagnostics")]
|
||||||
fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
|
||||||
use crate::util::read_file_string;
|
use crate::util::read_file_string;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
@@ -486,7 +507,7 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
|||||||
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?;
|
||||||
|
|
||||||
// Execute some environment checks
|
// Execute some environment checks
|
||||||
let running_within_docker = std::path::Path::new("/.dockerenv").exists() || std::path::Path::new("/run/.containerenv").exists();
|
let running_within_docker = is_running_in_docker();
|
||||||
let has_http_access = has_http_access();
|
let has_http_access = has_http_access();
|
||||||
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
let uses_proxy = env::var_os("HTTP_PROXY").is_some()
|
||||||
|| env::var_os("http_proxy").is_some()
|
|| env::var_os("http_proxy").is_some()
|
||||||
@@ -529,6 +550,11 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
|||||||
("-".to_string(), "-".to_string(), "-".to_string())
|
("-".to_string(), "-".to_string(), "-".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ip_header_name = match &ip_header.0 {
|
||||||
|
Some(h) => h,
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
let diagnostics_json = json!({
|
let diagnostics_json = json!({
|
||||||
"dns_resolved": dns_resolved,
|
"dns_resolved": dns_resolved,
|
||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version,
|
||||||
@@ -537,8 +563,13 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
|||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
"running_within_docker": running_within_docker,
|
"running_within_docker": running_within_docker,
|
||||||
"has_http_access": has_http_access,
|
"has_http_access": has_http_access,
|
||||||
|
"ip_header_exists": &ip_header.0.is_some(),
|
||||||
|
"ip_header_match": ip_header_name == CONFIG.ip_header(),
|
||||||
|
"ip_header_name": ip_header_name,
|
||||||
|
"ip_header_config": &CONFIG.ip_header(),
|
||||||
"uses_proxy": uses_proxy,
|
"uses_proxy": uses_proxy,
|
||||||
"db_type": *DB_TYPE,
|
"db_type": *DB_TYPE,
|
||||||
|
"db_version": get_sql_server_version(&conn),
|
||||||
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
|
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
|
||||||
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
|
||||||
});
|
});
|
||||||
@@ -548,9 +579,9 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/diagnostics/config")]
|
#[get("/diagnostics/config")]
|
||||||
fn get_diagnostics_config(_token: AdminToken) -> JsonResult {
|
fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
|
||||||
let support_json = CONFIG.get_support_json();
|
let support_json = CONFIG.get_support_json();
|
||||||
Ok(Json(support_json))
|
Json(support_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/config", data = "<data>")]
|
#[post("/config", data = "<data>")]
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
|
||||||
@@ -139,10 +140,8 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.last_verifying_at = Some(user.created_at);
|
user.last_verifying_at = Some(user.created_at);
|
||||||
} else {
|
} else if let Err(e) = mail::send_welcome(&user.email) {
|
||||||
if let Err(e) = mail::send_welcome(&user.email) {
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
error!("Error sending welcome email: {:#?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +149,8 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/accounts/profile")]
|
#[get("/accounts/profile")]
|
||||||
fn profile(headers: Headers, conn: DbConn) -> JsonResult {
|
fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(headers.user.to_json(&conn)))
|
Json(headers.user.to_json(&conn))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -612,7 +611,7 @@ struct PreloginData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/prelogin", data = "<data>")]
|
#[post("/accounts/prelogin", data = "<data>")]
|
||||||
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
||||||
let data: PreloginData = data.into_inner().data;
|
let data: PreloginData = data.into_inner().data;
|
||||||
|
|
||||||
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &conn) {
|
||||||
@@ -620,10 +619,10 @@ fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> JsonResult {
|
|||||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Kdf": kdf_type,
|
"Kdf": kdf_type,
|
||||||
"KdfIterations": kdf_iter
|
"KdfIterations": kdf_iter
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@@ -84,7 +84,7 @@ struct SyncData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sync?<data..>")]
|
#[get("/sync?<data..>")]
|
||||||
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let user_json = headers.user.to_json(&conn);
|
let user_json = headers.user.to_json(&conn);
|
||||||
|
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
@@ -104,25 +104,32 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let sends = Send::find_by_user(&headers.user.uuid, &conn);
|
||||||
|
let sends_json: Vec<Value> = sends
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_json())
|
||||||
|
.collect();
|
||||||
|
|
||||||
let domains_json = if data.exclude_domains {
|
let domains_json = if data.exclude_domains {
|
||||||
Value::Null
|
Value::Null
|
||||||
} else {
|
} else {
|
||||||
api::core::_get_eq_domains(headers, true).unwrap().into_inner()
|
api::core::_get_eq_domains(headers, true).into_inner()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Profile": user_json,
|
"Profile": user_json,
|
||||||
"Folders": folders_json,
|
"Folders": folders_json,
|
||||||
"Collections": collections_json,
|
"Collections": collections_json,
|
||||||
"Policies": policies_json,
|
"Policies": policies_json,
|
||||||
"Ciphers": ciphers_json,
|
"Ciphers": ciphers_json,
|
||||||
"Domains": domains_json,
|
"Domains": domains_json,
|
||||||
|
"Sends": sends_json,
|
||||||
"Object": "sync"
|
"Object": "sync"
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers")]
|
#[get("/ciphers")]
|
||||||
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_ciphers(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> = ciphers
|
||||||
@@ -130,11 +137,11 @@ fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<uuid>")]
|
#[get("/ciphers/<uuid>")]
|
||||||
@@ -278,19 +285,10 @@ fn enforce_personal_ownership_policy(
|
|||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
if data.OrganizationId.is_none() {
|
if data.OrganizationId.is_none() {
|
||||||
let user_uuid = &headers.user.uuid;
|
let user_uuid = &headers.user.uuid;
|
||||||
for policy in OrgPolicy::find_by_user(user_uuid, conn) {
|
let policy_type = OrgPolicyType::PersonalOwnership;
|
||||||
if policy.enabled && policy.has_type(OrgPolicyType::PersonalOwnership) {
|
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
|
||||||
let org_uuid = &policy.org_uuid;
|
err!("Due to an Enterprise Policy, you are restricted from \
|
||||||
match UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
saving items to your personal vault.")
|
||||||
Some(user) =>
|
|
||||||
if user.atype < UserOrgType::Admin &&
|
|
||||||
user.has_status(UserOrgStatus::Confirmed) {
|
|
||||||
err!("Due to an Enterprise Policy, you are restricted \
|
|
||||||
from saving items to your personal vault.")
|
|
||||||
},
|
|
||||||
None => err!("Error looking up user type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@@ -20,16 +20,16 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders")]
|
#[get("/folders")]
|
||||||
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||||
|
|
||||||
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": folders_json,
|
"Data": folders_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/folders/<uuid>")]
|
#[get("/folders/<uuid>")]
|
||||||
|
@@ -3,6 +3,9 @@ mod ciphers;
|
|||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
mod sends;
|
||||||
|
|
||||||
|
pub use sends::start_send_deletion_scheduler;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut mod_routes = routes![
|
let mut mod_routes = routes![
|
||||||
@@ -20,6 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes.append(&mut folders::routes());
|
routes.append(&mut folders::routes());
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
|
routes.append(&mut sends::routes());
|
||||||
routes.append(&mut mod_routes);
|
routes.append(&mut mod_routes);
|
||||||
|
|
||||||
routes
|
routes
|
||||||
@@ -30,17 +34,18 @@ pub fn routes() -> Vec<Route> {
|
|||||||
//
|
//
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
use rocket::response::Response;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult, JsonUpcase},
|
api::{JsonResult, JsonUpcase},
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
error::Error,
|
error::Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/clear-token")]
|
#[put("/devices/identifier/<uuid>/clear-token")]
|
||||||
fn clear_device_token(uuid: String) -> EmptyResult {
|
fn clear_device_token<'a>(uuid: String) -> Response<'a> {
|
||||||
// This endpoint doesn't have auth header
|
// This endpoint doesn't have auth header
|
||||||
|
|
||||||
let _ = uuid;
|
let _ = uuid;
|
||||||
@@ -49,11 +54,11 @@ fn clear_device_token(uuid: String) -> EmptyResult {
|
|||||||
// This only clears push token
|
// This only clears push token
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
|
||||||
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
|
||||||
Ok(())
|
Response::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
|
||||||
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> JsonResult {
|
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> Json<Value> {
|
||||||
let _data: Value = data.into_inner().data;
|
let _data: Value = data.into_inner().data;
|
||||||
// Data has a single string value "PushToken"
|
// Data has a single string value "PushToken"
|
||||||
let _ = uuid;
|
let _ = uuid;
|
||||||
@@ -61,13 +66,13 @@ fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) ->
|
|||||||
|
|
||||||
// TODO: This should save the push token, but we don't have push functionality
|
// TODO: This should save the push token, but we don't have push functionality
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Id": headers.device.uuid,
|
"Id": headers.device.uuid,
|
||||||
"Name": headers.device.name,
|
"Name": headers.device.name,
|
||||||
"Type": headers.device.atype,
|
"Type": headers.device.atype,
|
||||||
"Identifier": headers.device.uuid,
|
"Identifier": headers.device.uuid,
|
||||||
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
"CreationDate": crate::util::format_date(&headers.device.created_at),
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -81,11 +86,11 @@ struct GlobalDomain {
|
|||||||
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
|
||||||
|
|
||||||
#[get("/settings/domains")]
|
#[get("/settings/domains")]
|
||||||
fn get_eq_domains(headers: Headers) -> JsonResult {
|
fn get_eq_domains(headers: Headers) -> Json<Value> {
|
||||||
_get_eq_domains(headers, false)
|
_get_eq_domains(headers, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
|
fn _get_eq_domains(headers: Headers, no_excluded: bool) -> Json<Value> {
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
@@ -102,11 +107,11 @@ fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
|
|||||||
globals.retain(|g| !g.Excluded);
|
globals.retain(|g| !g.Excluded);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"EquivalentDomains": equivalent_domains,
|
"EquivalentDomains": equivalent_domains,
|
||||||
"GlobalEquivalentDomains": globals,
|
"GlobalEquivalentDomains": globals,
|
||||||
"Object": "domains",
|
"Object": "domains",
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@@ -192,8 +192,8 @@ fn post_organization(
|
|||||||
|
|
||||||
// GET /api/collections?writeOnly=false
|
// GET /api/collections?writeOnly=false
|
||||||
#[get("/collections")]
|
#[get("/collections")]
|
||||||
fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data":
|
"Data":
|
||||||
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
|
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -201,12 +201,12 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
|
|||||||
.collect::<Value>(),
|
.collect::<Value>(),
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/collections")]
|
#[get("/organizations/<org_id>/collections")]
|
||||||
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data":
|
"Data":
|
||||||
Collection::find_by_organization(&org_id, &conn)
|
Collection::find_by_organization(&org_id, &conn)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -214,7 +214,7 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
|
|||||||
.collect::<Value>(),
|
.collect::<Value>(),
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/collections", data = "<data>")]
|
#[post("/organizations/<org_id>/collections", data = "<data>")]
|
||||||
@@ -441,30 +441,30 @@ struct OrgIdData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/organization-details?<data..>")]
|
#[get("/ciphers/organization-details?<data..>")]
|
||||||
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
||||||
let ciphers_json: Vec<Value> = ciphers
|
let ciphers_json: Vec<Value> = ciphers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": ciphers_json,
|
"Data": ciphers_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/users")]
|
#[get("/organizations/<org_id>/users")]
|
||||||
fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
|
fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) -> Json<Value> {
|
||||||
let users = UserOrganization::find_by_org(&org_id, &conn);
|
let users = UserOrganization::find_by_org(&org_id, &conn);
|
||||||
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": users_json,
|
"Data": users_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -655,7 +655,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
|
|||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let mut org_name = String::from("bitwarden_rs");
|
let mut org_name = CONFIG.invitation_org_name();
|
||||||
if let Some(org_id) = &claims.org_id {
|
if let Some(org_id) = &claims.org_id {
|
||||||
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
org_name = match Organization::find_by_uuid(&org_id, &conn) {
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
@@ -930,15 +930,15 @@ fn post_org_import(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies")]
|
#[get("/organizations/<org_id>/policies")]
|
||||||
fn list_policies(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
fn list_policies(org_id: String, _headers: AdminHeaders, conn: DbConn) -> Json<Value> {
|
||||||
let policies = OrgPolicy::find_by_org(&org_id, &conn);
|
let policies = OrgPolicy::find_by_org(&org_id, &conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": policies_json,
|
"Data": policies_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies/token?<token>")]
|
#[get("/organizations/<org_id>/policies/token?<token>")]
|
||||||
@@ -1017,8 +1017,8 @@ fn get_organization_tax(org_id: String, _headers: Headers, _conn: DbConn) -> Emp
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/plans")]
|
#[get("/plans")]
|
||||||
fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn get_plans(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"Data": [
|
"Data": [
|
||||||
{
|
{
|
||||||
@@ -1065,17 +1065,17 @@ fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/plans/sales-tax-rates")]
|
#[get("/plans/sales-tax-rates")]
|
||||||
fn get_plans_tax_rates(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn get_plans_tax_rates(_headers: Headers, _conn: DbConn) -> Json<Value> {
|
||||||
// Prevent a 404 error, which also causes Javascript errors.
|
// Prevent a 404 error, which also causes Javascript errors.
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"Data": [],
|
"Data": [],
|
||||||
"ContinuationToken": null
|
"ContinuationToken": null
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -1128,7 +1128,7 @@ fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, con
|
|||||||
// If user is not part of the organization, but it exists
|
// If user is not part of the organization, but it exists
|
||||||
} else if UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &conn).is_none() {
|
} else if UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &conn).is_none() {
|
||||||
if let Some (user) = User::find_by_mail(&user_data.Email, &conn) {
|
if let Some (user) = User::find_by_mail(&user_data.Email, &conn) {
|
||||||
|
|
||||||
let user_org_status = if CONFIG.mail_enabled() {
|
let user_org_status = if CONFIG.mail_enabled() {
|
||||||
UserOrgStatus::Invited as i32
|
UserOrgStatus::Invited as i32
|
||||||
} else {
|
} else {
|
||||||
@@ -1157,18 +1157,18 @@ fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, con
|
|||||||
Some(headers.user.email.clone()),
|
Some(headers.user.email.clone()),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
|
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
|
||||||
if data.OverwriteExisting {
|
if data.OverwriteExisting {
|
||||||
for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn) {
|
for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn) {
|
||||||
if let Some (user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).map(|u| u.email) {
|
if let Some (user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).map(|u| u.email) {
|
||||||
if !data.Users.iter().any(|u| u.Email == user_email) {
|
if !data.Users.iter().any(|u| u.Email == user_email) {
|
||||||
user_org.delete(&conn)?;
|
user_org.delete(&conn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
416
src/api/core/sends.rs
Normal file
416
src/api/core/sends.rs
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
use std::{io::Read, path::Path};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
||||||
|
use rocket::{http::ContentType, Data};
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
|
||||||
|
auth::{Headers, Host},
|
||||||
|
db::{models::*, DbConn},
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
|
routes![
|
||||||
|
post_send,
|
||||||
|
post_send_file,
|
||||||
|
post_access,
|
||||||
|
post_access_file,
|
||||||
|
put_send,
|
||||||
|
delete_send,
|
||||||
|
put_remove_password
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_send_deletion_scheduler(pool: crate::db::DbPool) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
if let Ok(conn) = pool.get() {
|
||||||
|
info!("Initiating send deletion");
|
||||||
|
for send in Send::find_all(&conn) {
|
||||||
|
if chrono::Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
send.delete(&conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3600));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SendData {
|
||||||
|
pub Type: i32,
|
||||||
|
pub Key: String,
|
||||||
|
pub Password: Option<String>,
|
||||||
|
pub MaxAccessCount: Option<i32>,
|
||||||
|
pub ExpirationDate: Option<DateTime<Utc>>,
|
||||||
|
pub DeletionDate: DateTime<Utc>,
|
||||||
|
pub Disabled: bool,
|
||||||
|
|
||||||
|
// Data field
|
||||||
|
pub Name: String,
|
||||||
|
pub Notes: Option<String>,
|
||||||
|
pub Text: Option<Value>,
|
||||||
|
pub File: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
||||||
|
/// an org with this policy enabled isn't allowed to create new Sends or
|
||||||
|
/// modify existing ones, but is allowed to delete them.
|
||||||
|
///
|
||||||
|
/// Ref: https://bitwarden.com/help/article/policies/#disable-send
|
||||||
|
fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
|
||||||
|
let user_uuid = &headers.user.uuid;
|
||||||
|
let policy_type = OrgPolicyType::DisableSend;
|
||||||
|
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
|
||||||
|
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
||||||
|
let data_val = if data.Type == SendType::Text as i32 {
|
||||||
|
data.Text
|
||||||
|
} else if data.Type == SendType::File as i32 {
|
||||||
|
data.File
|
||||||
|
} else {
|
||||||
|
err!("Invalid Send type")
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_str = if let Some(mut d) = data_val {
|
||||||
|
d.as_object_mut().and_then(|o| o.remove("Response"));
|
||||||
|
serde_json::to_string(&d)?
|
||||||
|
} else {
|
||||||
|
err!("Send data not provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||||
|
err!(
|
||||||
|
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc());
|
||||||
|
send.user_uuid = Some(user_uuid);
|
||||||
|
send.notes = data.Notes;
|
||||||
|
send.max_access_count = data.MaxAccessCount;
|
||||||
|
send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc());
|
||||||
|
send.disabled = data.Disabled;
|
||||||
|
send.atype = data.Type;
|
||||||
|
|
||||||
|
send.set_password(data.Password.as_deref());
|
||||||
|
|
||||||
|
Ok(send)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends", data = "<data>")]
|
||||||
|
fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let data: SendData = data.into_inner().data;
|
||||||
|
|
||||||
|
if data.Type == SendType::File as i32 {
|
||||||
|
err!("File sends should use /api/sends/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut send = create_send(data, headers.user.uuid.clone())?;
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendCreate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
||||||
|
fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let boundary = content_type.params().next().expect("No boundary provided").1;
|
||||||
|
|
||||||
|
let mut mpart = Multipart::with_body(data.open(), boundary);
|
||||||
|
|
||||||
|
// First entry is the SendData JSON
|
||||||
|
let mut model_entry = match mpart.read_entry()? {
|
||||||
|
Some(e) if &*e.headers.name == "model" => e,
|
||||||
|
Some(_) => err!("Invalid entry name"),
|
||||||
|
None => err!("No model entry present"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
model_entry.data.read_to_string(&mut buf)?;
|
||||||
|
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
|
||||||
|
|
||||||
|
// Get the file length and add an extra 10% to avoid issues
|
||||||
|
const SIZE_110_MB: u64 = 115_343_360;
|
||||||
|
|
||||||
|
let size_limit = match CONFIG.user_attachment_limit() {
|
||||||
|
Some(0) => err!("File uploads are disabled"),
|
||||||
|
Some(limit_kb) => {
|
||||||
|
let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn);
|
||||||
|
if left <= 0 {
|
||||||
|
err!("Attachment size limit reached! Delete some files to open space")
|
||||||
|
}
|
||||||
|
std::cmp::Ord::max(left as u64, SIZE_110_MB)
|
||||||
|
}
|
||||||
|
None => SIZE_110_MB,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Send
|
||||||
|
let mut send = create_send(data.data, headers.user.uuid.clone())?;
|
||||||
|
let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32]));
|
||||||
|
|
||||||
|
if send.atype != SendType::File as i32 {
|
||||||
|
err!("Send content is not a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id);
|
||||||
|
|
||||||
|
// Read the data entry and save the file
|
||||||
|
let mut data_entry = match mpart.read_entry()? {
|
||||||
|
Some(e) if &*e.headers.name == "data" => e,
|
||||||
|
Some(_) => err!("Invalid entry name"),
|
||||||
|
None => err!("No model entry present"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = match data_entry
|
||||||
|
.data
|
||||||
|
.save()
|
||||||
|
.memory_threshold(0)
|
||||||
|
.size_limit(size_limit)
|
||||||
|
.with_path(&file_path)
|
||||||
|
{
|
||||||
|
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||||
|
SaveResult::Full(other) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Attachment is not a file: {:?}", other));
|
||||||
|
}
|
||||||
|
SaveResult::Partial(_, reason) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Attachment size limit exceeded with this file: {:?}", reason));
|
||||||
|
}
|
||||||
|
SaveResult::Error(e) => {
|
||||||
|
std::fs::remove_file(&file_path).ok();
|
||||||
|
err!(format!("Error: {:?}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set ID and sizes
|
||||||
|
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||||
|
if let Some(o) = data_value.as_object_mut() {
|
||||||
|
o.insert(String::from("Id"), Value::String(file_id));
|
||||||
|
o.insert(String::from("Size"), Value::Number(size.into()));
|
||||||
|
o.insert(
|
||||||
|
String::from("SizeName"),
|
||||||
|
Value::String(crate::util::get_display_size(size)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
send.data = serde_json::to_string(&data_value)?;
|
||||||
|
|
||||||
|
// Save the changes in the database
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendCreate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SendAccessData {
|
||||||
|
pub Password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||||
|
fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult {
|
||||||
|
let mut send = match Send::find_by_access_id(&access_id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_access_count) = send.max_access_count {
|
||||||
|
if send.access_count >= max_access_count {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = send.expiration_date {
|
||||||
|
if Utc::now().naive_utc() >= expiration {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.disabled {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.password_hash.is_some() {
|
||||||
|
match data.into_inner().data.Password {
|
||||||
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
|
Some(_) => err!("Invalid password."),
|
||||||
|
None => err_code!("Password not provided", 401),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files are incremented during the download
|
||||||
|
if send.atype == SendType::Text as i32 {
|
||||||
|
send.access_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(send.to_json_access()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||||
|
fn post_access_file(
|
||||||
|
send_id: String,
|
||||||
|
file_id: String,
|
||||||
|
data: JsonUpcase<SendAccessData>,
|
||||||
|
host: Host,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
let mut send = match Send::find_by_uuid(&send_id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_access_count) = send.max_access_count {
|
||||||
|
if send.access_count >= max_access_count {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expiration) = send.expiration_date {
|
||||||
|
if Utc::now().naive_utc() >= expiration {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Utc::now().naive_utc() >= send.deletion_date {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.disabled {
|
||||||
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.password_hash.is_some() {
|
||||||
|
match data.into_inner().data.Password {
|
||||||
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||||
|
Some(_) => err!("Invalid password."),
|
||||||
|
None => err_code!("Password not provided", 401),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send.access_count += 1;
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "send-fileDownload",
|
||||||
|
"Id": file_id,
|
||||||
|
"Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/sends/<id>", data = "<data>")]
|
||||||
|
fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let data: SendData = data.into_inner().data;
|
||||||
|
|
||||||
|
let mut send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if send.atype != data.Type {
|
||||||
|
err!("Sends can't change type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// When updating a file Send, we receive nulls in the File field, as it's immutable,
|
||||||
|
// so we only need to update the data field in the Text case
|
||||||
|
if data.Type == SendType::Text as i32 {
|
||||||
|
let data_str = if let Some(mut d) = data.Text {
|
||||||
|
d.as_object_mut().and_then(|d| d.remove("Response"));
|
||||||
|
serde_json::to_string(&d)?
|
||||||
|
} else {
|
||||||
|
err!("Send data not provided");
|
||||||
|
};
|
||||||
|
send.data = data_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.DeletionDate > Utc::now() + Duration::days(31) {
|
||||||
|
err!(
|
||||||
|
"You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
send.name = data.Name;
|
||||||
|
send.akey = data.Key;
|
||||||
|
send.deletion_date = data.DeletionDate.naive_utc();
|
||||||
|
send.notes = data.Notes;
|
||||||
|
send.max_access_count = data.MaxAccessCount;
|
||||||
|
send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc());
|
||||||
|
send.disabled = data.Disabled;
|
||||||
|
|
||||||
|
// Only change the value if it's present
|
||||||
|
if let Some(password) = data.Password {
|
||||||
|
send.set_password(Some(&password));
|
||||||
|
}
|
||||||
|
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/sends/<id>")]
|
||||||
|
fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||||
|
let send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
send.delete(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendDelete, &headers.user);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/sends/<id>/remove-password")]
|
||||||
|
fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult {
|
||||||
|
enforce_disable_send_policy(&headers, &conn)?;
|
||||||
|
|
||||||
|
let mut send = match Send::find_by_uuid(&id, &conn) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => err!("Send not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
||||||
|
err!("Send is not owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
send.set_password(None);
|
||||||
|
send.save(&conn)?;
|
||||||
|
nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user);
|
||||||
|
|
||||||
|
Ok(Json(send.to_json()))
|
||||||
|
}
|
@@ -38,15 +38,15 @@ pub fn routes() -> Vec<Route> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/two-factor")]
|
#[get("/two-factor")]
|
||||||
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
|
fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
|
||||||
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"Data": twofactors_json,
|
"Data": twofactors_json,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-recover", data = "<data>")]
|
#[post("/two-factor/get-recover", data = "<data>")]
|
||||||
|
@@ -131,12 +131,12 @@ struct RegisterResponseCopy {
|
|||||||
pub error_code: Option<NumberOrString>,
|
pub error_code: Option<NumberOrString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<RegisterResponse> for RegisterResponseCopy {
|
impl From<RegisterResponseCopy> for RegisterResponse {
|
||||||
fn into(self) -> RegisterResponse {
|
fn from(r: RegisterResponseCopy) -> RegisterResponse {
|
||||||
RegisterResponse {
|
RegisterResponse {
|
||||||
registration_data: self.registration_data,
|
registration_data: r.registration_data,
|
||||||
version: self.version,
|
version: r.version,
|
||||||
client_data: self.client_data,
|
client_data: r.client_data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,6 @@ use once_cell::sync::Lazy;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{blocking::Client, blocking::Response, header, Url};
|
use reqwest::{blocking::Client, blocking::Response, header, Url};
|
||||||
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
|
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
|
||||||
use soup::prelude::*;
|
|
||||||
|
|
||||||
use crate::{error::Error, util::Cached, CONFIG};
|
use crate::{error::Error, util::Cached, CONFIG};
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ fn icon(domain: String) -> Option<Cached<Content<Vec<u8>>>> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_icon(&domain).map(|icon| Cached::long(Content(ContentType::new("image", "x-icon"), icon)))
|
get_icon(&domain).map(|icon| Cached::ttl(Content(ContentType::new("image", "x-icon"), icon), CONFIG.icon_cache_ttl()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns if the domain provided is valid or not.
|
/// Returns if the domain provided is valid or not.
|
||||||
@@ -332,6 +331,44 @@ impl Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_favicons_node(node: &std::rc::Rc<markup5ever_rcdom::Node>, icons: &mut Vec<Icon>, url: &Url) {
|
||||||
|
if let markup5ever_rcdom::NodeData::Element { name, attrs, .. } = &node.data {
|
||||||
|
if name.local.as_ref() == "link" {
|
||||||
|
let mut has_rel = false;
|
||||||
|
let mut href = None;
|
||||||
|
let mut sizes = None;
|
||||||
|
|
||||||
|
let attrs = attrs.borrow();
|
||||||
|
for attr in attrs.iter() {
|
||||||
|
let attr_name = attr.name.local.as_ref();
|
||||||
|
let attr_value = attr.value.as_ref();
|
||||||
|
|
||||||
|
if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) {
|
||||||
|
has_rel = true;
|
||||||
|
} else if attr_name == "href" {
|
||||||
|
href = Some(attr_value);
|
||||||
|
} else if attr_name == "sizes" {
|
||||||
|
sizes = Some(attr_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_rel {
|
||||||
|
if let Some(inner_href) = href {
|
||||||
|
if let Ok(full_href) = url.join(&inner_href).map(|h| h.into_string()) {
|
||||||
|
let priority = get_icon_priority(&full_href, sizes);
|
||||||
|
icons.push(Icon::new(priority, full_href));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Might want to limit the recursion depth?
|
||||||
|
for child in node.children.borrow().iter() {
|
||||||
|
get_favicons_node(child, icons, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct IconUrlResult {
|
struct IconUrlResult {
|
||||||
iconlist: Vec<Icon>,
|
iconlist: Vec<Icon>,
|
||||||
cookies: String,
|
cookies: String,
|
||||||
@@ -431,30 +468,14 @@ fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
|
|
||||||
// 512KB should be more than enough for the HTML, though as we only really need
|
// 512KB should be more than enough for the HTML, though as we only really need
|
||||||
// the HTML header, it could potentially be reduced even further
|
// the HTML header, it could potentially be reduced even further
|
||||||
let limited_reader = content.take(512 * 1024);
|
let mut limited_reader = content.take(512 * 1024);
|
||||||
|
|
||||||
let soup = Soup::from_reader(limited_reader)?;
|
use html5ever::tendril::TendrilSink;
|
||||||
// Search for and filter
|
let dom = html5ever::parse_document(markup5ever_rcdom::RcDom::default(), Default::default())
|
||||||
let favicons = soup
|
.from_utf8()
|
||||||
.tag("link")
|
.read_from(&mut limited_reader)?;
|
||||||
.attr("rel", ICON_REL_REGEX.clone()) // Only use icon rels
|
|
||||||
.attr_name("href") // Make sure there is a href
|
|
||||||
.find_all();
|
|
||||||
|
|
||||||
// Loop through all the found icons and determine it's priority
|
get_favicons_node(&dom.document, &mut iconlist, &url);
|
||||||
for favicon in favicons {
|
|
||||||
let sizes = favicon.get("sizes");
|
|
||||||
let href = favicon.get("href").unwrap();
|
|
||||||
// Skip invalid url's
|
|
||||||
let full_href = match url.join(&href) {
|
|
||||||
Ok(h) => h.into_string(),
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let priority = get_icon_priority(&full_href, sizes);
|
|
||||||
|
|
||||||
iconlist.push(Icon::new(priority, full_href))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Add the default favicon.ico to the list with just the given domain
|
// Add the default favicon.ico to the list with just the given domain
|
||||||
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
|
||||||
@@ -506,7 +527,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str, referer: &str) -> Result<R
|
|||||||
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
|
/// 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", "");
|
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
|
||||||
/// ```
|
/// ```
|
||||||
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
fn get_icon_priority(href: &str, sizes: Option<&str>) -> u8 {
|
||||||
// Check if there is a dimension set
|
// Check if there is a dimension set
|
||||||
let (width, height) = parse_sizes(sizes);
|
let (width, height) = parse_sizes(sizes);
|
||||||
|
|
||||||
@@ -554,7 +575,7 @@ fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
|
|||||||
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
|
||||||
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
/// let (width, height) = parse_sizes("32"); // (0, 0)
|
||||||
/// ```
|
/// ```
|
||||||
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
|
fn parse_sizes(sizes: Option<&str>) -> (u16, u16) {
|
||||||
let mut width: u16 = 0;
|
let mut width: u16 = 0;
|
||||||
let mut height: u16 = 0;
|
let mut height: u16 = 0;
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ use serde_json::Value;
|
|||||||
pub use crate::api::{
|
pub use crate::api::{
|
||||||
admin::routes as admin_routes,
|
admin::routes as admin_routes,
|
||||||
core::routes as core_routes,
|
core::routes as core_routes,
|
||||||
|
core::start_send_deletion_scheduler,
|
||||||
icons::routes as icons_routes,
|
icons::routes as icons_routes,
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
notifications::routes as notifications_routes,
|
notifications::routes as notifications_routes,
|
||||||
|
@@ -5,7 +5,7 @@ use rocket_contrib::json::Json;
|
|||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{EmptyResult, JsonResult},
|
api::EmptyResult,
|
||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
Error, CONFIG,
|
Error, CONFIG,
|
||||||
@@ -31,7 +31,7 @@ fn websockets_err() -> EmptyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/hub/negotiate")]
|
#[post("/hub/negotiate")]
|
||||||
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> {
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use data_encoding::BASE64URL;
|
use data_encoding::BASE64URL;
|
||||||
|
|
||||||
@@ -47,10 +47,10 @@ fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
|
|||||||
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
|
||||||
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
|
||||||
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
|
||||||
Ok(Json(json!({
|
Json(json!({
|
||||||
"connectionId": conn_id,
|
"connectionId": conn_id,
|
||||||
"availableTransports": available_transports
|
"availableTransports": available_transports
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -120,7 +120,7 @@ fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Server WebSocket handler
|
// Server WebSocket handler
|
||||||
pub struct WSHandler {
|
pub struct WsHandler {
|
||||||
out: Sender,
|
out: Sender,
|
||||||
user_uuid: Option<String>,
|
user_uuid: Option<String>,
|
||||||
users: WebSocketUsers,
|
users: WebSocketUsers,
|
||||||
@@ -140,7 +140,7 @@ const PING: Token = Token(1);
|
|||||||
|
|
||||||
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
const ACCESS_TOKEN_KEY: &str = "access_token=";
|
||||||
|
|
||||||
impl WSHandler {
|
impl WsHandler {
|
||||||
fn err(&self, msg: &'static str) -> ws::Result<()> {
|
fn err(&self, msg: &'static str) -> ws::Result<()> {
|
||||||
self.out.close(ws::CloseCode::Invalid)?;
|
self.out.close(ws::CloseCode::Invalid)?;
|
||||||
|
|
||||||
@@ -166,8 +166,8 @@ impl WSHandler {
|
|||||||
if let Some(params) = path.split('?').nth(1) {
|
if let Some(params) = path.split('?').nth(1) {
|
||||||
let params_iter = params.split('&').take(1);
|
let params_iter = params.split('&').take(1);
|
||||||
for val in params_iter {
|
for val in params_iter {
|
||||||
if val.starts_with(ACCESS_TOKEN_KEY) {
|
if let Some(stripped) = val.strip_prefix(ACCESS_TOKEN_KEY) {
|
||||||
return Some(val[ACCESS_TOKEN_KEY.len()..].into());
|
return Some(stripped.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -176,7 +176,7 @@ impl WSHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler for WSHandler {
|
impl Handler for WsHandler {
|
||||||
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
|
||||||
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
|
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
|
||||||
//
|
//
|
||||||
@@ -240,13 +240,13 @@ impl Handler for WSHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WSFactory {
|
struct WsFactory {
|
||||||
pub users: WebSocketUsers,
|
pub users: WebSocketUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WSFactory {
|
impl WsFactory {
|
||||||
pub fn init() -> Self {
|
pub fn init() -> Self {
|
||||||
WSFactory {
|
WsFactory {
|
||||||
users: WebSocketUsers {
|
users: WebSocketUsers {
|
||||||
map: Arc::new(CHashMap::new()),
|
map: Arc::new(CHashMap::new()),
|
||||||
},
|
},
|
||||||
@@ -254,11 +254,11 @@ impl WSFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Factory for WSFactory {
|
impl Factory for WsFactory {
|
||||||
type Handler = WSHandler;
|
type Handler = WsHandler;
|
||||||
|
|
||||||
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
fn connection_made(&mut self, out: Sender) -> Self::Handler {
|
||||||
WSHandler {
|
WsHandler {
|
||||||
out,
|
out,
|
||||||
user_uuid: None,
|
user_uuid: None,
|
||||||
users: self.users.clone(),
|
users: self.users.clone(),
|
||||||
@@ -394,6 +394,10 @@ pub enum UpdateType {
|
|||||||
|
|
||||||
LogOut = 11,
|
LogOut = 11,
|
||||||
|
|
||||||
|
SyncSendCreate = 12,
|
||||||
|
SyncSendUpdate = 13,
|
||||||
|
SyncSendDelete = 14,
|
||||||
|
|
||||||
None = 100,
|
None = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,15 +405,17 @@ use rocket::State;
|
|||||||
pub type Notify<'a> = State<'a, WebSocketUsers>;
|
pub type Notify<'a> = State<'a, WebSocketUsers>;
|
||||||
|
|
||||||
pub fn start_notification_server() -> WebSocketUsers {
|
pub fn start_notification_server() -> WebSocketUsers {
|
||||||
let factory = WSFactory::init();
|
let factory = WsFactory::init();
|
||||||
let users = factory.users.clone();
|
let users = factory.users.clone();
|
||||||
|
|
||||||
if CONFIG.websocket_enabled() {
|
if CONFIG.websocket_enabled() {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut settings = ws::Settings::default();
|
let settings = ws::Settings {
|
||||||
settings.max_connections = 500;
|
max_connections: 500,
|
||||||
settings.queue_size = 2;
|
queue_size: 2,
|
||||||
settings.panic_on_internal = false;
|
panic_on_internal: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
ws::Builder::new()
|
ws::Builder::new()
|
||||||
.with_settings(settings)
|
.with_settings(settings)
|
||||||
|
@@ -10,7 +10,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
// If addding more routes here, consider also adding them to
|
// If addding more routes here, consider also adding them to
|
||||||
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
||||||
if CONFIG.web_vault_enabled() {
|
if CONFIG.web_vault_enabled() {
|
||||||
routes![web_index, app_id, web_files, attachments, alive, static_files]
|
routes![web_index, app_id, web_files, attachments, sends, alive, static_files]
|
||||||
} else {
|
} else {
|
||||||
routes![attachments, alive, static_files]
|
routes![attachments, alive, static_files]
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,11 @@ fn attachments(uuid: String, file: PathBuf) -> Option<NamedFile> {
|
|||||||
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok()
|
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/sends/<send_id>/<file_id>")]
|
||||||
|
fn sends(send_id: String, file_id: String) -> Option<NamedFile> {
|
||||||
|
NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/alive")]
|
#[get("/alive")]
|
||||||
fn alive() -> Json<String> {
|
fn alive() -> Json<String> {
|
||||||
use crate::util::format_date;
|
use crate::util::format_date;
|
||||||
@@ -79,7 +84,6 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
|
|||||||
|
|
||||||
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
|
||||||
"bootstrap-native.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
"bootstrap-native.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native.js"))),
|
||||||
"md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))),
|
|
||||||
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
|
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
|
||||||
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
|
||||||
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
|
||||||
|
97
src/auth.rs
97
src/auth.rs
@@ -58,28 +58,28 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
|||||||
.map_res("Error decoding JWT")
|
.map_res("Error decoding JWT")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_login(token: &str) -> Result<LoginJWTClaims, Error> {
|
pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
decode_jwt(token, JWT_LOGIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_invite(token: &str) -> Result<InviteJWTClaims, Error> {
|
pub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
decode_jwt(token, JWT_INVITE_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_delete(token: &str) -> Result<DeleteJWTClaims, Error> {
|
pub fn decode_delete(token: &str) -> Result<DeleteJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_DELETE_ISSUER.to_string())
|
decode_jwt(token, JWT_DELETE_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJWTClaims, Error> {
|
pub fn decode_verify_email(token: &str) -> Result<VerifyEmailJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
|
decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_admin(token: &str) -> Result<AdminJWTClaims, Error> {
|
pub fn decode_admin(token: &str) -> Result<AdminJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
decode_jwt(token, JWT_ADMIN_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJWTClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -110,7 +110,7 @@ pub struct LoginJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct InviteJWTClaims {
|
pub struct InviteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -132,9 +132,9 @@ pub fn generate_invite_claims(
|
|||||||
org_id: Option<String>,
|
org_id: Option<String>,
|
||||||
user_org_id: Option<String>,
|
user_org_id: Option<String>,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
) -> InviteJWTClaims {
|
) -> InviteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
InviteJWTClaims {
|
InviteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_INVITE_ISSUER.to_string(),
|
iss: JWT_INVITE_ISSUER.to_string(),
|
||||||
@@ -147,7 +147,7 @@ pub fn generate_invite_claims(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct DeleteJWTClaims {
|
pub struct DeleteJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -158,9 +158,9 @@ pub struct DeleteJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
|
pub fn generate_delete_claims(uuid: String) -> DeleteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
DeleteJWTClaims {
|
DeleteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_DELETE_ISSUER.to_string(),
|
iss: JWT_DELETE_ISSUER.to_string(),
|
||||||
@@ -169,7 +169,7 @@ pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct VerifyEmailJWTClaims {
|
pub struct VerifyEmailJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -180,9 +180,9 @@ pub struct VerifyEmailJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
|
pub fn generate_verify_email_claims(uuid: String) -> DeleteJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
DeleteJWTClaims {
|
DeleteJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::days(5)).timestamp(),
|
exp: (time_now + Duration::days(5)).timestamp(),
|
||||||
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
|
||||||
@@ -191,7 +191,7 @@ pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AdminJWTClaims {
|
pub struct AdminJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
pub nbf: i64,
|
pub nbf: i64,
|
||||||
// Expiration time
|
// Expiration time
|
||||||
@@ -202,9 +202,9 @@ pub struct AdminJWTClaims {
|
|||||||
pub sub: String,
|
pub sub: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_admin_claims() -> AdminJWTClaims {
|
pub fn generate_admin_claims() -> AdminJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
AdminJWTClaims {
|
AdminJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + Duration::minutes(20)).timestamp(),
|
exp: (time_now + Duration::minutes(20)).timestamp(),
|
||||||
iss: JWT_ADMIN_ISSUER.to_string(),
|
iss: JWT_ADMIN_ISSUER.to_string(),
|
||||||
@@ -222,13 +222,12 @@ use crate::db::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Headers {
|
pub struct Host {
|
||||||
pub host: String,
|
pub host: String
|
||||||
pub device: Device,
|
|
||||||
pub user: User,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for Host {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
@@ -262,6 +261,28 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
|||||||
format!("{}://{}", protocol, host)
|
format!("{}://{}", protocol, host)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Host { host })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Headers {
|
||||||
|
pub host: String,
|
||||||
|
pub device: Device,
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
let host = match Host::from_request(request) {
|
||||||
|
Outcome::Forward(_) => return Outcome::Forward(()),
|
||||||
|
Outcome::Failure(f) => return Outcome::Failure(f),
|
||||||
|
Outcome::Success(host) => host.host,
|
||||||
|
};
|
||||||
|
|
||||||
// Get access_token
|
// Get access_token
|
||||||
let access_token: &str = match headers.get_one("Authorization") {
|
let access_token: &str = match headers.get_one("Authorization") {
|
||||||
Some(a) => match a.rsplit("Bearer ").next() {
|
Some(a) => match a.rsplit("Bearer ").next() {
|
||||||
@@ -429,12 +450,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Headers> for AdminHeaders {
|
impl From<AdminHeaders> for Headers {
|
||||||
fn into(self) -> Headers {
|
fn from(h: AdminHeaders) -> Headers {
|
||||||
Headers {
|
Headers {
|
||||||
host: self.host,
|
host: h.host,
|
||||||
device: self.device,
|
device: h.device,
|
||||||
user: self.user,
|
user: h.user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,12 +529,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Headers> for ManagerHeaders {
|
impl From<ManagerHeaders> for Headers {
|
||||||
fn into(self) -> Headers {
|
fn from(h: ManagerHeaders) -> Headers {
|
||||||
Headers {
|
Headers {
|
||||||
host: self.host,
|
host: h.host,
|
||||||
device: self.device,
|
device: h.device,
|
||||||
user: self.user,
|
user: h.user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,12 +571,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeadersLoose {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Headers> for ManagerHeadersLoose {
|
impl From<ManagerHeadersLoose> for Headers {
|
||||||
fn into(self) -> Headers {
|
fn from(h: ManagerHeadersLoose) -> Headers {
|
||||||
Headers {
|
Headers {
|
||||||
host: self.host,
|
host: h.host,
|
||||||
device: self.device,
|
device: h.device,
|
||||||
user: self.user,
|
user: h.user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -299,6 +299,8 @@ make_config! {
|
|||||||
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
|
||||||
/// Attachments folder
|
/// Attachments folder
|
||||||
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
|
||||||
|
/// Sends folder
|
||||||
|
sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends");
|
||||||
/// Templates folder
|
/// Templates folder
|
||||||
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
|
||||||
/// Session JWT key
|
/// Session JWT key
|
||||||
|
@@ -37,6 +37,7 @@ macro_rules! generate_connections {
|
|||||||
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
|
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
|
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
|
||||||
|
|
||||||
impl DbPool {
|
impl DbPool {
|
||||||
@@ -124,16 +125,34 @@ macro_rules! db_run {
|
|||||||
$($(
|
$($(
|
||||||
#[cfg($db)]
|
#[cfg($db)]
|
||||||
crate::db::DbConn::$db(ref $conn) => {
|
crate::db::DbConn::$db(ref $conn) => {
|
||||||
paste::paste! {
|
paste::paste! {
|
||||||
#[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *};
|
#[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *};
|
||||||
#[allow(unused)] use [<__ $db _model>]::*;
|
#[allow(unused)] use [<__ $db _model>]::*;
|
||||||
#[allow(unused)] use crate::db::FromDb;
|
#[allow(unused)] use crate::db::FromDb;
|
||||||
}
|
}
|
||||||
$body
|
$body
|
||||||
},
|
},
|
||||||
)+)+
|
)+)+
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Same for all dbs
|
||||||
|
( @raw $conn:ident: $body:block ) => {
|
||||||
|
db_run! { @raw $conn: sqlite, mysql, postgresql $body }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different code for each db
|
||||||
|
( @raw $conn:ident: $( $($db:ident),+ $body:block )+ ) => {
|
||||||
|
#[allow(unused)] use diesel::prelude::*;
|
||||||
|
match $conn {
|
||||||
|
$($(
|
||||||
|
#[cfg($db)]
|
||||||
|
crate::db::DbConn::$db(ref $conn) => {
|
||||||
|
$body
|
||||||
|
},
|
||||||
|
)+)+
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -143,7 +162,7 @@ pub trait FromDb {
|
|||||||
fn from_db(self) -> Self::Output;
|
fn from_db(self) -> Self::Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql),
|
// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql),
|
||||||
// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run!
|
// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run!
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! db_object {
|
macro_rules! db_object {
|
||||||
@@ -153,10 +172,10 @@ macro_rules! db_object {
|
|||||||
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
|
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
|
||||||
$(,)?
|
$(,)?
|
||||||
}
|
}
|
||||||
)+ ) => {
|
)+ ) => {
|
||||||
// Create the normal struct, without attributes
|
// Create the normal struct, without attributes
|
||||||
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
|
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
|
||||||
|
|
||||||
#[cfg(sqlite)]
|
#[cfg(sqlite)]
|
||||||
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
|
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
|
||||||
#[cfg(mysql)]
|
#[cfg(mysql)]
|
||||||
@@ -177,7 +196,7 @@ macro_rules! db_object {
|
|||||||
)+ }
|
)+ }
|
||||||
|
|
||||||
impl [<$name Db>] {
|
impl [<$name Db>] {
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
#[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } }
|
#[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +240,36 @@ pub fn backup_database() -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
use diesel::sql_types::Text;
|
||||||
|
#[derive(QueryableByName,Debug)]
|
||||||
|
struct SqlVersion {
|
||||||
|
#[sql_type = "Text"]
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the SQL Server version
|
||||||
|
pub fn get_sql_server_version(conn: &DbConn) -> String {
|
||||||
|
db_run! {@raw conn:
|
||||||
|
postgresql, mysql {
|
||||||
|
match diesel::sql_query("SELECT version() AS version;").get_result::<SqlVersion>(conn).ok() {
|
||||||
|
Some(v) => {
|
||||||
|
v.version
|
||||||
|
},
|
||||||
|
_ => "Unknown".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite {
|
||||||
|
match diesel::sql_query("SELECT sqlite_version() AS version;").get_result::<SqlVersion>(conn).ok() {
|
||||||
|
Some(v) => {
|
||||||
|
v.version
|
||||||
|
},
|
||||||
|
_ => "Unknown".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempts to retrieve a single connection from the managed database pool. If
|
/// Attempts to retrieve a single connection from the managed database pool. If
|
||||||
/// no pool is currently managed, fails with an `InternalServerError` status. If
|
/// no pool is currently managed, fails with an `InternalServerError` status. If
|
||||||
/// no connections are available, fails with a `ServiceUnavailable` status.
|
/// no connections are available, fails with a `ServiceUnavailable` status.
|
||||||
@@ -262,7 +311,7 @@ mod sqlite_migrations {
|
|||||||
let connection =
|
let connection =
|
||||||
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
|
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
// Disable Foreign Key Checks during migration
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
// Scoped to a connection.
|
// Scoped to a connection.
|
||||||
diesel::sql_query("PRAGMA foreign_keys = OFF")
|
diesel::sql_query("PRAGMA foreign_keys = OFF")
|
||||||
.execute(&connection)
|
.execute(&connection)
|
||||||
@@ -313,7 +362,7 @@ mod postgresql_migrations {
|
|||||||
let connection =
|
let connection =
|
||||||
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
|
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
|
||||||
// Disable Foreign Key Checks during migration
|
// Disable Foreign Key Checks during migration
|
||||||
|
|
||||||
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
|
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
|
||||||
// "SET CONSTRAINTS sets the behavior of constraint checking within the
|
// "SET CONSTRAINTS sets the behavior of constraint checking within the
|
||||||
// current transaction", so this setting probably won't take effect for
|
// current transaction", so this setting probably won't take effect for
|
||||||
|
@@ -4,7 +4,7 @@ use super::Cipher;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "attachments"]
|
#[table_name = "attachments"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
|
||||||
|
@@ -14,7 +14,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "ciphers"]
|
#[table_name = "ciphers"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
@@ -104,7 +104,7 @@ impl Cipher {
|
|||||||
|
|
||||||
// Get the type_data or a default to an empty json object '{}'.
|
// Get the type_data or a default to an empty json object '{}'.
|
||||||
// If not passing an empty object, mobile clients will crash.
|
// If not passing an empty object, mobile clients will crash.
|
||||||
let mut type_data_json: Value = serde_json::from_str(&self.data).unwrap_or(json!({}));
|
let mut type_data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({}));
|
||||||
|
|
||||||
// NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream
|
// NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream
|
||||||
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
|
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.
|
||||||
|
@@ -3,7 +3,7 @@ use serde_json::Value;
|
|||||||
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
|
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "collections"]
|
#[table_name = "collections"]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -13,7 +13,7 @@ db_object! {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "users_collections"]
|
#[table_name = "users_collections"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
@@ -25,7 +25,7 @@ db_object! {
|
|||||||
pub hide_passwords: bool,
|
pub hide_passwords: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "ciphers_collections"]
|
#[table_name = "ciphers_collections"]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
#[belongs_to(Collection, foreign_key = "collection_uuid")]
|
||||||
|
@@ -4,7 +4,7 @@ use super::User;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "devices"]
|
#[table_name = "devices"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
@@ -80,8 +80,8 @@ impl Device {
|
|||||||
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
|
||||||
|
|
||||||
// Create the JWT claims struct, to send to the client
|
// Create the JWT claims struct, to send to the client
|
||||||
use crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
|
||||||
let claims = LoginJWTClaims {
|
let claims = LoginJwtClaims {
|
||||||
nbf: time_now.timestamp(),
|
nbf: time_now.timestamp(),
|
||||||
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
|
||||||
iss: JWT_LOGIN_ISSUER.to_string(),
|
iss: JWT_LOGIN_ISSUER.to_string(),
|
||||||
@@ -117,7 +117,7 @@ impl Device {
|
|||||||
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
self.updated_at = Utc::now().naive_utc();
|
self.updated_at = Utc::now().naive_utc();
|
||||||
|
|
||||||
db_run! { conn:
|
db_run! { conn:
|
||||||
sqlite, mysql {
|
sqlite, mysql {
|
||||||
crate::util::retry(
|
crate::util::retry(
|
||||||
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
|
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use super::{Cipher, User};
|
use super::{Cipher, User};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "favorites"]
|
#[table_name = "favorites"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
|
@@ -4,7 +4,7 @@ use serde_json::Value;
|
|||||||
use super::{Cipher, User};
|
use super::{Cipher, User};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "folders"]
|
#[table_name = "folders"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -16,7 +16,7 @@ db_object! {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
|
#[derive(Identifiable, Queryable, Insertable, Associations)]
|
||||||
#[table_name = "folders_ciphers"]
|
#[table_name = "folders_ciphers"]
|
||||||
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
|
||||||
#[belongs_to(Folder, foreign_key = "folder_uuid")]
|
#[belongs_to(Folder, foreign_key = "folder_uuid")]
|
||||||
|
@@ -8,6 +8,7 @@ mod org_policy;
|
|||||||
mod organization;
|
mod organization;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
mod user;
|
mod user;
|
||||||
|
mod send;
|
||||||
|
|
||||||
pub use self::attachment::Attachment;
|
pub use self::attachment::Attachment;
|
||||||
pub use self::cipher::Cipher;
|
pub use self::cipher::Cipher;
|
||||||
@@ -19,3 +20,4 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyType};
|
|||||||
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::user::{Invitation, User, UserStampException};
|
pub use self::user::{Invitation, User, UserStampException};
|
||||||
|
pub use self::send::{Send, SendType};
|
@@ -4,10 +4,10 @@ use crate::api::EmptyResult;
|
|||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
use super::{Organization, UserOrgStatus};
|
use super::{Organization, UserOrganization, UserOrgStatus, UserOrgType};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "org_policies"]
|
#[table_name = "org_policies"]
|
||||||
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
#[belongs_to(Organization, foreign_key = "org_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -20,7 +20,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[derive(Copy, Clone)]
|
||||||
#[derive(num_derive::FromPrimitive)]
|
#[derive(num_derive::FromPrimitive)]
|
||||||
pub enum OrgPolicyType {
|
pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
@@ -29,6 +29,7 @@ pub enum OrgPolicyType {
|
|||||||
// SingleOrg = 3, // Not currently supported.
|
// SingleOrg = 3, // Not currently supported.
|
||||||
// RequireSso = 4, // Not currently supported.
|
// RequireSso = 4, // Not currently supported.
|
||||||
PersonalOwnership = 5,
|
PersonalOwnership = 5,
|
||||||
|
DisableSend = 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
@@ -170,6 +171,23 @@ impl OrgPolicy {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the user belongs to an org that has enabled the specified policy type,
|
||||||
|
/// and the user is not an owner or admin of that org. This is only useful for checking
|
||||||
|
/// applicability of policy types that have these particular semantics.
|
||||||
|
pub fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
|
||||||
|
for policy in OrgPolicy::find_by_user(user_uuid, conn) { // Returns confirmed users only.
|
||||||
|
if policy.enabled && policy.has_type(policy_type) {
|
||||||
|
let org_uuid = &policy.org_uuid;
|
||||||
|
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
||||||
|
if user.atype < UserOrgType::Admin {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
|
||||||
|
@@ -5,7 +5,7 @@ use num_traits::FromPrimitive;
|
|||||||
use super::{CollectionUser, User, OrgPolicy};
|
use super::{CollectionUser, User, OrgPolicy};
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "organizations"]
|
#[table_name = "organizations"]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct Organization {
|
pub struct Organization {
|
||||||
@@ -14,7 +14,7 @@ db_object! {
|
|||||||
pub billing_email: String,
|
pub billing_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users_organizations"]
|
#[table_name = "users_organizations"]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
pub struct UserOrganization {
|
pub struct UserOrganization {
|
||||||
@@ -90,17 +90,11 @@ impl PartialOrd<i32> for UserOrgType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gt(&self, other: &i32) -> bool {
|
fn gt(&self, other: &i32) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Greater))
|
||||||
Some(Ordering::Less) | Some(Ordering::Equal) => false,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ge(&self, other: &i32) -> bool {
|
fn ge(&self, other: &i32) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Greater) | Some(Ordering::Equal))
|
||||||
Some(Ordering::Less) => false,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,17 +113,11 @@ impl PartialOrd<UserOrgType> for i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lt(&self, other: &UserOrgType) -> bool {
|
fn lt(&self, other: &UserOrgType) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Less) | None)
|
||||||
Some(Ordering::Less) | None => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn le(&self, other: &UserOrgType) -> bool {
|
fn le(&self, other: &UserOrgType) -> bool {
|
||||||
match self.partial_cmp(other) {
|
matches!(self.partial_cmp(other), Some(Ordering::Less) | Some(Ordering::Equal) | None)
|
||||||
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
274
src/db/models/send.rs
Normal file
274
src/db/models/send.rs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::{Organization, User};
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
|
#[table_name = "sends"]
|
||||||
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
|
#[belongs_to(Organization, foreign_key = "organization_uuid")]
|
||||||
|
#[primary_key(uuid)]
|
||||||
|
pub struct Send {
|
||||||
|
pub uuid: String,
|
||||||
|
|
||||||
|
pub user_uuid: Option<String>,
|
||||||
|
pub organization_uuid: Option<String>,
|
||||||
|
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
|
||||||
|
pub atype: i32,
|
||||||
|
pub data: String,
|
||||||
|
pub akey: String,
|
||||||
|
pub password_hash: Option<Vec<u8>>,
|
||||||
|
password_salt: Option<Vec<u8>>,
|
||||||
|
password_iter: Option<i32>,
|
||||||
|
|
||||||
|
pub max_access_count: Option<i32>,
|
||||||
|
pub access_count: i32,
|
||||||
|
|
||||||
|
pub creation_date: NaiveDateTime,
|
||||||
|
pub revision_date: NaiveDateTime,
|
||||||
|
pub expiration_date: Option<NaiveDateTime>,
|
||||||
|
pub deletion_date: NaiveDateTime,
|
||||||
|
|
||||||
|
pub disabled: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
|
||||||
|
pub enum SendType {
|
||||||
|
Text = 0,
|
||||||
|
File = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Send {
|
||||||
|
pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
uuid: crate::util::get_uuid(),
|
||||||
|
user_uuid: None,
|
||||||
|
organization_uuid: None,
|
||||||
|
|
||||||
|
name,
|
||||||
|
notes: None,
|
||||||
|
|
||||||
|
atype,
|
||||||
|
data,
|
||||||
|
akey,
|
||||||
|
password_hash: None,
|
||||||
|
password_salt: None,
|
||||||
|
password_iter: None,
|
||||||
|
|
||||||
|
max_access_count: None,
|
||||||
|
access_count: 0,
|
||||||
|
|
||||||
|
creation_date: now,
|
||||||
|
revision_date: now,
|
||||||
|
expiration_date: None,
|
||||||
|
deletion_date,
|
||||||
|
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_password(&mut self, password: Option<&str>) {
|
||||||
|
const PASSWORD_ITER: i32 = 100_000;
|
||||||
|
|
||||||
|
if let Some(password) = password {
|
||||||
|
self.password_iter = Some(PASSWORD_ITER);
|
||||||
|
let salt = crate::crypto::get_random_64();
|
||||||
|
let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32);
|
||||||
|
self.password_salt = Some(salt);
|
||||||
|
self.password_hash = Some(hash);
|
||||||
|
} else {
|
||||||
|
self.password_iter = None;
|
||||||
|
self.password_salt = None;
|
||||||
|
self.password_hash = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_password(&self, password: &str) -> bool {
|
||||||
|
match (&self.password_hash, &self.password_salt, self.password_iter) {
|
||||||
|
(Some(hash), Some(salt), Some(iter)) => {
|
||||||
|
crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
use crate::util::format_date;
|
||||||
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"Id": self.uuid,
|
||||||
|
"AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
||||||
|
"Type": self.atype,
|
||||||
|
|
||||||
|
"Name": self.name,
|
||||||
|
"Notes": self.notes,
|
||||||
|
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||||
|
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||||
|
|
||||||
|
"Key": self.akey,
|
||||||
|
"MaxAccessCount": self.max_access_count,
|
||||||
|
"AccessCount": self.access_count,
|
||||||
|
"Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
||||||
|
"Disabled": self.disabled,
|
||||||
|
|
||||||
|
"RevisionDate": format_date(&self.revision_date),
|
||||||
|
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||||
|
"DeletionDate": format_date(&self.deletion_date),
|
||||||
|
"Object": "send",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json_access(&self) -> Value {
|
||||||
|
use crate::util::format_date;
|
||||||
|
|
||||||
|
let data: Value = serde_json::from_str(&self.data).unwrap_or_default();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"Id": self.uuid,
|
||||||
|
"Type": self.atype,
|
||||||
|
|
||||||
|
"Name": self.name,
|
||||||
|
"Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
||||||
|
"File": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
||||||
|
|
||||||
|
"ExpirationDate": self.expiration_date.as_ref().map(format_date),
|
||||||
|
"Object": "send-access",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::db::DbConn;
|
||||||
|
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
impl Send {
|
||||||
|
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
||||||
|
self.update_users_revision(conn);
|
||||||
|
self.revision_date = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
match diesel::replace_into(sends::table)
|
||||||
|
.values(SendDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
||||||
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
||||||
|
diesel::update(sends::table)
|
||||||
|
.filter(sends::uuid.eq(&self.uuid))
|
||||||
|
.set(SendDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = SendDb::to_db(self);
|
||||||
|
diesel::insert_into(sends::table)
|
||||||
|
.values(&value)
|
||||||
|
.on_conflict(sends::uuid)
|
||||||
|
.do_update()
|
||||||
|
.set(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, conn: &DbConn) -> EmptyResult {
|
||||||
|
self.update_users_revision(conn);
|
||||||
|
|
||||||
|
if self.atype == SendType::File as i32 {
|
||||||
|
std::fs::remove_dir_all(std::path::Path::new(&crate::CONFIG.sends_folder()).join(&self.uuid)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting send")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_users_revision(&self, conn: &DbConn) {
|
||||||
|
match &self.user_uuid {
|
||||||
|
Some(user_uuid) => {
|
||||||
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Belongs to Organization, not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
||||||
|
for send in Self::find_by_user(user_uuid, &conn) {
|
||||||
|
send.delete(&conn)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_all(conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uuid = match Uuid::from_slice(&uuid_vec) {
|
||||||
|
Ok(u) => u.to_string(),
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::find_by_uuid(&uuid, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::uuid.eq(uuid))
|
||||||
|
.first::<SendDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::user_uuid.eq(user_uuid))
|
||||||
|
.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
sends::table
|
||||||
|
.filter(sends::organization_uuid.eq(org_uuid))
|
||||||
|
.load::<SendDb>(conn).expect("Error loading sends").from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ use crate::error::MapResult;
|
|||||||
use super::User;
|
use super::User;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
|
||||||
#[table_name = "twofactor"]
|
#[table_name = "twofactor"]
|
||||||
#[belongs_to(User, foreign_key = "user_uuid")]
|
#[belongs_to(User, foreign_key = "user_uuid")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
|
@@ -5,7 +5,7 @@ use crate::crypto;
|
|||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
#[changeset_options(treat_none_as_null="true")]
|
#[changeset_options(treat_none_as_null="true")]
|
||||||
#[primary_key(uuid)]
|
#[primary_key(uuid)]
|
||||||
@@ -47,7 +47,7 @@ db_object! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
#[table_name = "invitations"]
|
#[table_name = "invitations"]
|
||||||
#[primary_key(email)]
|
#[primary_key(email)]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
@@ -177,7 +177,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization};
|
use super::{Cipher, Device, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization};
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
@@ -263,6 +263,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Send::delete_all_by_user(&self.uuid, conn)?;
|
||||||
UserOrganization::delete_all_by_user(&self.uuid, conn)?;
|
UserOrganization::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Cipher::delete_all_by_user(&self.uuid, conn)?;
|
Cipher::delete_all_by_user(&self.uuid, conn)?;
|
||||||
Favorite::delete_all_by_user(&self.uuid, conn)?;
|
Favorite::delete_all_by_user(&self.uuid, conn)?;
|
||||||
|
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Datetime,
|
||||||
|
revision_date -> Datetime,
|
||||||
|
expiration_date -> Nullable<Datetime>,
|
||||||
|
deletion_date -> Datetime,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
expiration_date -> Nullable<Timestamp>,
|
||||||
|
deletion_date -> Timestamp,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
@@ -102,6 +102,29 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sends (uuid) {
|
||||||
|
uuid -> Text,
|
||||||
|
user_uuid -> Nullable<Text>,
|
||||||
|
organization_uuid -> Nullable<Text>,
|
||||||
|
name -> Text,
|
||||||
|
notes -> Nullable<Text>,
|
||||||
|
atype -> Integer,
|
||||||
|
data -> Text,
|
||||||
|
akey -> Text,
|
||||||
|
password_hash -> Nullable<Binary>,
|
||||||
|
password_salt -> Nullable<Binary>,
|
||||||
|
password_iter -> Nullable<Integer>,
|
||||||
|
max_access_count -> Nullable<Integer>,
|
||||||
|
access_count -> Integer,
|
||||||
|
creation_date -> Timestamp,
|
||||||
|
revision_date -> Timestamp,
|
||||||
|
expiration_date -> Nullable<Timestamp>,
|
||||||
|
deletion_date -> Timestamp,
|
||||||
|
disabled -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
twofactor (uuid) {
|
twofactor (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
|
|||||||
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
joinable!(folders_ciphers -> ciphers (cipher_uuid));
|
||||||
joinable!(folders_ciphers -> folders (folder_uuid));
|
joinable!(folders_ciphers -> folders (folder_uuid));
|
||||||
joinable!(org_policies -> organizations (org_uuid));
|
joinable!(org_policies -> organizations (org_uuid));
|
||||||
|
joinable!(sends -> organizations (organization_uuid));
|
||||||
|
joinable!(sends -> users (user_uuid));
|
||||||
joinable!(twofactor -> users (user_uuid));
|
joinable!(twofactor -> users (user_uuid));
|
||||||
joinable!(users_collections -> collections (collection_uuid));
|
joinable!(users_collections -> collections (collection_uuid));
|
||||||
joinable!(users_collections -> users (user_uuid));
|
joinable!(users_collections -> users (user_uuid));
|
||||||
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
org_policies,
|
org_policies,
|
||||||
organizations,
|
organizations,
|
||||||
|
sends,
|
||||||
twofactor,
|
twofactor,
|
||||||
users,
|
users,
|
||||||
users_collections,
|
users_collections,
|
||||||
|
21
src/error.rs
21
src/error.rs
@@ -38,11 +38,11 @@ use diesel::ConnectionError as DieselConErr;
|
|||||||
use diesel_migrations::RunMigrationsError as DieselMigErr;
|
use diesel_migrations::RunMigrationsError as DieselMigErr;
|
||||||
use diesel::r2d2::PoolError as R2d2Err;
|
use diesel::r2d2::PoolError as R2d2Err;
|
||||||
use handlebars::RenderError as HbErr;
|
use handlebars::RenderError as HbErr;
|
||||||
use jsonwebtoken::errors::Error as JWTErr;
|
use jsonwebtoken::errors::Error as JwtErr;
|
||||||
use regex::Error as RegexErr;
|
use regex::Error as RegexErr;
|
||||||
use reqwest::Error as ReqErr;
|
use reqwest::Error as ReqErr;
|
||||||
use serde_json::{Error as SerdeErr, Value};
|
use serde_json::{Error as SerdeErr, Value};
|
||||||
use std::io::Error as IOErr;
|
use std::io::Error as IoErr;
|
||||||
|
|
||||||
use std::time::SystemTimeError as TimeErr;
|
use std::time::SystemTimeError as TimeErr;
|
||||||
use u2f::u2ferror::U2fError as U2fErr;
|
use u2f::u2ferror::U2fError as U2fErr;
|
||||||
@@ -72,10 +72,10 @@ make_error! {
|
|||||||
R2d2Error(R2d2Err): _has_source, _api_error,
|
R2d2Error(R2d2Err): _has_source, _api_error,
|
||||||
U2fError(U2fErr): _has_source, _api_error,
|
U2fError(U2fErr): _has_source, _api_error,
|
||||||
SerdeError(SerdeErr): _has_source, _api_error,
|
SerdeError(SerdeErr): _has_source, _api_error,
|
||||||
JWTError(JWTErr): _has_source, _api_error,
|
JWtError(JwtErr): _has_source, _api_error,
|
||||||
TemplError(HbErr): _has_source, _api_error,
|
TemplError(HbErr): _has_source, _api_error,
|
||||||
//WsError(ws::Error): _has_source, _api_error,
|
//WsError(ws::Error): _has_source, _api_error,
|
||||||
IOError(IOErr): _has_source, _api_error,
|
IoError(IoErr): _has_source, _api_error,
|
||||||
TimeError(TimeErr): _has_source, _api_error,
|
TimeError(TimeErr): _has_source, _api_error,
|
||||||
ReqError(ReqErr): _has_source, _api_error,
|
ReqError(ReqErr): _has_source, _api_error,
|
||||||
RegexError(RegexErr): _has_source, _api_error,
|
RegexError(RegexErr): _has_source, _api_error,
|
||||||
@@ -152,6 +152,7 @@ impl<S> MapResult<S> for Option<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
const fn _has_source<T>(e: T) -> Option<T> {
|
const fn _has_source<T>(e: T) -> Option<T> {
|
||||||
Some(e)
|
Some(e)
|
||||||
}
|
}
|
||||||
@@ -220,6 +221,18 @@ macro_rules! err {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! err_code {
|
||||||
|
($msg:expr, $err_code: literal) => {{
|
||||||
|
error!("{}", $msg);
|
||||||
|
return Err(crate::error::Error::new($msg, $msg).with_code($err_code));
|
||||||
|
}};
|
||||||
|
($usr_msg:expr, $log_value:expr, $err_code: literal) => {{
|
||||||
|
error!("{}. {}", $usr_msg, $log_value);
|
||||||
|
return Err(crate::error::Error::new($usr_msg, $log_value).with_code($err_code));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! err_discard {
|
macro_rules! err_discard {
|
||||||
($msg:expr, $data:expr) => {{
|
($msg:expr, $data:expr) => {{
|
||||||
|
68
src/mail.rs
68
src/mail.rs
@@ -30,10 +30,10 @@ fn mailer() -> SmtpTransport {
|
|||||||
let smtp_client = if CONFIG.smtp_ssl() {
|
let smtp_client = if CONFIG.smtp_ssl() {
|
||||||
let mut tls_parameters = TlsParameters::builder(host);
|
let mut tls_parameters = TlsParameters::builder(host);
|
||||||
if CONFIG.smtp_accept_invalid_hostnames() {
|
if CONFIG.smtp_accept_invalid_hostnames() {
|
||||||
tls_parameters.dangerous_accept_invalid_hostnames(true);
|
tls_parameters = tls_parameters.dangerous_accept_invalid_hostnames(true);
|
||||||
}
|
}
|
||||||
if CONFIG.smtp_accept_invalid_certs() {
|
if CONFIG.smtp_accept_invalid_certs() {
|
||||||
tls_parameters.dangerous_accept_invalid_certs(true);
|
tls_parameters = tls_parameters.dangerous_accept_invalid_certs(true);
|
||||||
}
|
}
|
||||||
let tls_parameters = tls_parameters.build().unwrap();
|
let tls_parameters = tls_parameters.build().unwrap();
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ fn mailer() -> SmtpTransport {
|
|||||||
|
|
||||||
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
let smtp_client = match CONFIG.smtp_auth_mechanism() {
|
||||||
Some(mechanism) => {
|
Some(mechanism) => {
|
||||||
let allowed_mechanisms = vec![SmtpAuthMechanism::Plain, SmtpAuthMechanism::Login, SmtpAuthMechanism::Xoauth2];
|
let allowed_mechanisms = [SmtpAuthMechanism::Plain, SmtpAuthMechanism::Login, SmtpAuthMechanism::Xoauth2];
|
||||||
let mut selected_mechanisms = vec![];
|
let mut selected_mechanisms = vec![];
|
||||||
for wanted_mechanism in mechanism.split(',') {
|
for wanted_mechanism in mechanism.split(',') {
|
||||||
for m in &allowed_mechanisms {
|
for m in &allowed_mechanisms {
|
||||||
@@ -115,7 +115,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
|
|||||||
|
|
||||||
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -132,7 +132,7 @@ pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -149,7 +149,7 @@ pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome(address: &str) -> EmptyResult {
|
pub fn send_welcome(address: &str) -> EmptyResult {
|
||||||
@@ -160,7 +160,7 @@ pub fn send_welcome(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
||||||
@@ -176,7 +176,7 @@ pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite(
|
pub fn send_invite(
|
||||||
@@ -200,15 +200,15 @@ pub fn send_invite(
|
|||||||
"email/send_org_invite",
|
"email/send_org_invite",
|
||||||
json!({
|
json!({
|
||||||
"url": CONFIG.domain(),
|
"url": CONFIG.domain(),
|
||||||
"org_id": org_id.unwrap_or_else(|| "_".to_string()),
|
"org_id": org_id.as_deref().unwrap_or("_"),
|
||||||
"org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()),
|
"org_user_id": org_user_id.as_deref().unwrap_or("_"),
|
||||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||||
"org_name": org_name,
|
"org_name": org_name,
|
||||||
"token": invite_token,
|
"token": invite_token,
|
||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -221,7 +221,7 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str)
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||||
@@ -233,7 +233,7 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
|
pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>, device: &str) -> EmptyResult {
|
||||||
@@ -251,7 +251,7 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime<Local>,
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -263,7 +263,7 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
||||||
@@ -275,7 +275,7 @@ pub fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_test(address: &str) -> EmptyResult {
|
pub fn send_test(address: &str) -> EmptyResult {
|
||||||
@@ -286,10 +286,10 @@ pub fn send_test(address: &str) -> EmptyResult {
|
|||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_email(address, &subject, &body_html, &body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
|
||||||
let address_split: Vec<&str> = address.rsplitn(2, '@').collect();
|
let address_split: Vec<&str> = address.rsplitn(2, '@').collect();
|
||||||
if address_split.len() != 2 {
|
if address_split.len() != 2 {
|
||||||
err!("Invalid email address (no @)");
|
err!("Invalid email address (no @)");
|
||||||
@@ -306,13 +306,13 @@ fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) ->
|
|||||||
// We force Base64 encoding because in the past we had issues with different encodings.
|
// We force Base64 encoding because in the past we had issues with different encodings.
|
||||||
.header(header::ContentTransferEncoding::Base64)
|
.header(header::ContentTransferEncoding::Base64)
|
||||||
.header(header::ContentType("text/html; charset=utf-8".parse()?))
|
.header(header::ContentType("text/html; charset=utf-8".parse()?))
|
||||||
.body(String::from(body_html));
|
.body(body_html);
|
||||||
|
|
||||||
let text = SinglePart::builder()
|
let text = SinglePart::builder()
|
||||||
// We force Base64 encoding because in the past we had issues with different encodings.
|
// We force Base64 encoding because in the past we had issues with different encodings.
|
||||||
.header(header::ContentTransferEncoding::Base64)
|
.header(header::ContentTransferEncoding::Base64)
|
||||||
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
|
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
|
||||||
.body(String::from(body_text));
|
.body(body_text);
|
||||||
|
|
||||||
let smtp_from = &CONFIG.smtp_from();
|
let smtp_from = &CONFIG.smtp_from();
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
@@ -332,21 +332,19 @@ fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) ->
|
|||||||
match mailer().send(&email) {
|
match mailer().send(&email) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
// Match some common errors and make them more user friendly
|
// Match some common errors and make them more user friendly
|
||||||
Err(e) => match e {
|
Err(e) => {
|
||||||
lettre::transport::smtp::Error::Client(x) => {
|
|
||||||
err!(format!("SMTP Client error: {}", x));
|
if e.is_client() {
|
||||||
},
|
err!(format!("SMTP Client error: {}", e));
|
||||||
lettre::transport::smtp::Error::Transient(x) => {
|
} else if e.is_transient() {
|
||||||
err!(format!("SMTP 4xx error: {:?}", x.message));
|
err!(format!("SMTP 4xx error: {:?}", e));
|
||||||
},
|
} else if e.is_permanent() {
|
||||||
lettre::transport::smtp::Error::Permanent(x) => {
|
err!(format!("SMTP 5xx error: {:?}", e));
|
||||||
err!(format!("SMTP 5xx error: {:?}", x.message));
|
} else if e.is_timeout() {
|
||||||
},
|
err!(format!("SMTP timeout error: {:?}", e));
|
||||||
lettre::transport::smtp::Error::Io(x) => {
|
} else {
|
||||||
err!(format!("SMTP IO error: {}", x));
|
Err(e.into())
|
||||||
},
|
}
|
||||||
// Fallback for all other errors
|
|
||||||
_ => Err(e.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
src/main.rs
71
src/main.rs
@@ -25,8 +25,6 @@ use std::{
|
|||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use structopt::StructOpt;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod error;
|
mod error;
|
||||||
mod api;
|
mod api;
|
||||||
@@ -40,14 +38,7 @@ mod util;
|
|||||||
|
|
||||||
pub use config::CONFIG;
|
pub use config::CONFIG;
|
||||||
pub use error::{Error, MapResult};
|
pub use error::{Error, MapResult};
|
||||||
|
pub use util::is_running_in_docker;
|
||||||
#[derive(Debug, StructOpt)]
|
|
||||||
#[structopt(name = "bitwarden_rs", about = "A Bitwarden API server written in Rust")]
|
|
||||||
struct Opt {
|
|
||||||
/// Prints the app version
|
|
||||||
#[structopt(short, long)]
|
|
||||||
version: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
parse_args();
|
parse_args();
|
||||||
@@ -57,11 +48,9 @@ fn main() {
|
|||||||
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
|
||||||
init_logging(level).ok();
|
init_logging(level).ok();
|
||||||
|
|
||||||
let extra_debug = match level {
|
let extra_debug = matches!(level, LF::Trace | LF::Debug);
|
||||||
LF::Trace | LF::Debug => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
check_data_folder();
|
||||||
check_rsa_keys();
|
check_rsa_keys();
|
||||||
check_web_vault();
|
check_web_vault();
|
||||||
|
|
||||||
@@ -70,14 +59,27 @@ fn main() {
|
|||||||
launch_rocket(extra_debug);
|
launch_rocket(extra_debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HELP: &str = "\
|
||||||
|
A Bitwarden API server written in Rust
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
bitwarden_rs
|
||||||
|
|
||||||
|
FLAGS:
|
||||||
|
-h, --help Prints help information
|
||||||
|
-v, --version Prints the app version
|
||||||
|
";
|
||||||
|
|
||||||
fn parse_args() {
|
fn parse_args() {
|
||||||
let opt = Opt::from_args();
|
const NO_VERSION: &str = "(Version info from Git not present)";
|
||||||
if opt.version {
|
let mut pargs = pico_args::Arguments::from_env();
|
||||||
if let Some(version) = option_env!("BWRS_VERSION") {
|
|
||||||
println!("bitwarden_rs {}", version);
|
if pargs.contains(["-h", "--help"]) {
|
||||||
} else {
|
println!("bitwarden_rs {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
|
||||||
println!("bitwarden_rs (Version info from Git not present)");
|
print!("{}", HELP);
|
||||||
}
|
exit(0);
|
||||||
|
} else if pargs.contains(["-v", "--version"]) {
|
||||||
|
println!("bitwarden_rs {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,9 +214,28 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir(path: &str, description: &str) {
|
||||||
|
// Try to create the specified dir, if it doesn't already exist.
|
||||||
|
let err_msg = format!("Error creating {} directory '{}'", description, path);
|
||||||
|
create_dir_all(path).expect(&err_msg);
|
||||||
|
}
|
||||||
|
|
||||||
fn create_icon_cache_folder() {
|
fn create_icon_cache_folder() {
|
||||||
// Try to create the icon cache folder, and generate an error if it could not.
|
create_dir(&CONFIG.icon_cache_folder(), "icon cache");
|
||||||
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory");
|
}
|
||||||
|
|
||||||
|
fn check_data_folder() {
|
||||||
|
let data_folder = &CONFIG.data_folder();
|
||||||
|
let path = Path::new(data_folder);
|
||||||
|
if !path.exists() {
|
||||||
|
error!("Data folder '{}' doesn't exist.", data_folder);
|
||||||
|
if is_running_in_docker() {
|
||||||
|
error!("Verify that your data volume is mounted at the correct location.");
|
||||||
|
} else {
|
||||||
|
error!("Create the data folder and try again.");
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_rsa_keys() {
|
fn check_rsa_keys() {
|
||||||
@@ -289,6 +310,8 @@ fn launch_rocket(extra_debug: bool) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api::start_send_deletion_scheduler(pool.clone());
|
||||||
|
|
||||||
let basepath = &CONFIG.domain_path();
|
let basepath = &CONFIG.domain_path();
|
||||||
|
|
||||||
// If adding more paths here, consider also adding them to
|
// If adding more paths here, consider also adding them to
|
||||||
@@ -303,7 +326,7 @@ fn launch_rocket(extra_debug: bool) {
|
|||||||
.manage(pool)
|
.manage(pool)
|
||||||
.manage(api::start_notification_server())
|
.manage(api::start_notification_server())
|
||||||
.attach(util::AppHeaders())
|
.attach(util::AppHeaders())
|
||||||
.attach(util::CORS())
|
.attach(util::Cors())
|
||||||
.attach(util::BetterLogging(extra_debug))
|
.attach(util::BetterLogging(extra_debug))
|
||||||
.launch();
|
.launch();
|
||||||
|
|
||||||
|
8
src/static/global_domains.json
vendored
8
src/static/global_domains.json
vendored
@@ -894,5 +894,13 @@
|
|||||||
"sony.com"
|
"sony.com"
|
||||||
],
|
],
|
||||||
"Excluded": false
|
"Excluded": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": 85,
|
||||||
|
"Domains": [
|
||||||
|
"protonmail.com",
|
||||||
|
"protonvpn.com"
|
||||||
|
],
|
||||||
|
"Excluded": false
|
||||||
}
|
}
|
||||||
]
|
]
|
@@ -1,402 +0,0 @@
|
|||||||
/*
|
|
||||||
* JavaScript MD5
|
|
||||||
* https://github.com/blueimp/JavaScript-MD5
|
|
||||||
*
|
|
||||||
* Copyright 2011, Sebastian Tschan
|
|
||||||
* https://blueimp.net
|
|
||||||
*
|
|
||||||
* Licensed under the MIT license:
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*
|
|
||||||
* Based on
|
|
||||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
|
||||||
* Digest Algorithm, as defined in RFC 1321.
|
|
||||||
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
|
||||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
|
||||||
* Distributed under the BSD License
|
|
||||||
* See http://pajhome.org.uk/crypt/md5 for more info.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* global define */
|
|
||||||
|
|
||||||
/* eslint-disable strict */
|
|
||||||
|
|
||||||
;(function($) {
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add integers, wrapping at 2^32.
|
|
||||||
* This uses 16-bit operations internally to work around bugs in interpreters.
|
|
||||||
*
|
|
||||||
* @param {number} x First integer
|
|
||||||
* @param {number} y Second integer
|
|
||||||
* @returns {number} Sum
|
|
||||||
*/
|
|
||||||
function safeAdd(x, y) {
|
|
||||||
var lsw = (x & 0xffff) + (y & 0xffff)
|
|
||||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
|
||||||
return (msw << 16) | (lsw & 0xffff)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bitwise rotate a 32-bit number to the left.
|
|
||||||
*
|
|
||||||
* @param {number} num 32-bit number
|
|
||||||
* @param {number} cnt Rotation count
|
|
||||||
* @returns {number} Rotated number
|
|
||||||
*/
|
|
||||||
function bitRotateLeft(num, cnt) {
|
|
||||||
return (num << cnt) | (num >>> (32 - cnt))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic operation the algorithm uses.
|
|
||||||
*
|
|
||||||
* @param {number} q q
|
|
||||||
* @param {number} a a
|
|
||||||
* @param {number} b b
|
|
||||||
* @param {number} x x
|
|
||||||
* @param {number} s s
|
|
||||||
* @param {number} t t
|
|
||||||
* @returns {number} Result
|
|
||||||
*/
|
|
||||||
function md5cmn(q, a, b, x, s, t) {
|
|
||||||
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Basic operation the algorithm uses.
|
|
||||||
*
|
|
||||||
* @param {number} a a
|
|
||||||
* @param {number} b b
|
|
||||||
* @param {number} c c
|
|
||||||
* @param {number} d d
|
|
||||||
* @param {number} x x
|
|
||||||
* @param {number} s s
|
|
||||||
* @param {number} t t
|
|
||||||
* @returns {number} Result
|
|
||||||
*/
|
|
||||||
function md5ff(a, b, c, d, x, s, t) {
|
|
||||||
return md5cmn((b & c) | (~b & d), a, b, x, s, t)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Basic operation the algorithm uses.
|
|
||||||
*
|
|
||||||
* @param {number} a a
|
|
||||||
* @param {number} b b
|
|
||||||
* @param {number} c c
|
|
||||||
* @param {number} d d
|
|
||||||
* @param {number} x x
|
|
||||||
* @param {number} s s
|
|
||||||
* @param {number} t t
|
|
||||||
* @returns {number} Result
|
|
||||||
*/
|
|
||||||
function md5gg(a, b, c, d, x, s, t) {
|
|
||||||
return md5cmn((b & d) | (c & ~d), a, b, x, s, t)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Basic operation the algorithm uses.
|
|
||||||
*
|
|
||||||
* @param {number} a a
|
|
||||||
* @param {number} b b
|
|
||||||
* @param {number} c c
|
|
||||||
* @param {number} d d
|
|
||||||
* @param {number} x x
|
|
||||||
* @param {number} s s
|
|
||||||
* @param {number} t t
|
|
||||||
* @returns {number} Result
|
|
||||||
*/
|
|
||||||
function md5hh(a, b, c, d, x, s, t) {
|
|
||||||
return md5cmn(b ^ c ^ d, a, b, x, s, t)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Basic operation the algorithm uses.
|
|
||||||
*
|
|
||||||
* @param {number} a a
|
|
||||||
* @param {number} b b
|
|
||||||
* @param {number} c c
|
|
||||||
* @param {number} d d
|
|
||||||
* @param {number} x x
|
|
||||||
* @param {number} s s
|
|
||||||
* @param {number} t t
|
|
||||||
* @returns {number} Result
|
|
||||||
*/
|
|
||||||
function md5ii(a, b, c, d, x, s, t) {
|
|
||||||
return md5cmn(c ^ (b | ~d), a, b, x, s, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the MD5 of an array of little-endian words, and a bit length.
|
|
||||||
*
|
|
||||||
* @param {Array} x Array of little-endian words
|
|
||||||
* @param {number} len Bit length
|
|
||||||
* @returns {Array<number>} MD5 Array
|
|
||||||
*/
|
|
||||||
function binlMD5(x, len) {
|
|
||||||
/* append padding */
|
|
||||||
x[len >> 5] |= 0x80 << len % 32
|
|
||||||
x[(((len + 64) >>> 9) << 4) + 14] = len
|
|
||||||
|
|
||||||
var i
|
|
||||||
var olda
|
|
||||||
var oldb
|
|
||||||
var oldc
|
|
||||||
var oldd
|
|
||||||
var a = 1732584193
|
|
||||||
var b = -271733879
|
|
||||||
var c = -1732584194
|
|
||||||
var d = 271733878
|
|
||||||
|
|
||||||
for (i = 0; i < x.length; i += 16) {
|
|
||||||
olda = a
|
|
||||||
oldb = b
|
|
||||||
oldc = c
|
|
||||||
oldd = d
|
|
||||||
|
|
||||||
a = md5ff(a, b, c, d, x[i], 7, -680876936)
|
|
||||||
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586)
|
|
||||||
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819)
|
|
||||||
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330)
|
|
||||||
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897)
|
|
||||||
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426)
|
|
||||||
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341)
|
|
||||||
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983)
|
|
||||||
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416)
|
|
||||||
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417)
|
|
||||||
c = md5ff(c, d, a, b, x[i + 10], 17, -42063)
|
|
||||||
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162)
|
|
||||||
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682)
|
|
||||||
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101)
|
|
||||||
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290)
|
|
||||||
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329)
|
|
||||||
|
|
||||||
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510)
|
|
||||||
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632)
|
|
||||||
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713)
|
|
||||||
b = md5gg(b, c, d, a, x[i], 20, -373897302)
|
|
||||||
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691)
|
|
||||||
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083)
|
|
||||||
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335)
|
|
||||||
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848)
|
|
||||||
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438)
|
|
||||||
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690)
|
|
||||||
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961)
|
|
||||||
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501)
|
|
||||||
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467)
|
|
||||||
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784)
|
|
||||||
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473)
|
|
||||||
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734)
|
|
||||||
|
|
||||||
a = md5hh(a, b, c, d, x[i + 5], 4, -378558)
|
|
||||||
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463)
|
|
||||||
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562)
|
|
||||||
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556)
|
|
||||||
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060)
|
|
||||||
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353)
|
|
||||||
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632)
|
|
||||||
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640)
|
|
||||||
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174)
|
|
||||||
d = md5hh(d, a, b, c, x[i], 11, -358537222)
|
|
||||||
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979)
|
|
||||||
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189)
|
|
||||||
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487)
|
|
||||||
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835)
|
|
||||||
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520)
|
|
||||||
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651)
|
|
||||||
|
|
||||||
a = md5ii(a, b, c, d, x[i], 6, -198630844)
|
|
||||||
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415)
|
|
||||||
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905)
|
|
||||||
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055)
|
|
||||||
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571)
|
|
||||||
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606)
|
|
||||||
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523)
|
|
||||||
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799)
|
|
||||||
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359)
|
|
||||||
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744)
|
|
||||||
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380)
|
|
||||||
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649)
|
|
||||||
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070)
|
|
||||||
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379)
|
|
||||||
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259)
|
|
||||||
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551)
|
|
||||||
|
|
||||||
a = safeAdd(a, olda)
|
|
||||||
b = safeAdd(b, oldb)
|
|
||||||
c = safeAdd(c, oldc)
|
|
||||||
d = safeAdd(d, oldd)
|
|
||||||
}
|
|
||||||
return [a, b, c, d]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an array of little-endian words to a string
|
|
||||||
*
|
|
||||||
* @param {Array<number>} input MD5 Array
|
|
||||||
* @returns {string} MD5 string
|
|
||||||
*/
|
|
||||||
function binl2rstr(input) {
|
|
||||||
var i
|
|
||||||
var output = ''
|
|
||||||
var length32 = input.length * 32
|
|
||||||
for (i = 0; i < length32; i += 8) {
|
|
||||||
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff)
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a raw string to an array of little-endian words
|
|
||||||
* Characters >255 have their high-byte silently ignored.
|
|
||||||
*
|
|
||||||
* @param {string} input Raw input string
|
|
||||||
* @returns {Array<number>} Array of little-endian words
|
|
||||||
*/
|
|
||||||
function rstr2binl(input) {
|
|
||||||
var i
|
|
||||||
var output = []
|
|
||||||
output[(input.length >> 2) - 1] = undefined
|
|
||||||
for (i = 0; i < output.length; i += 1) {
|
|
||||||
output[i] = 0
|
|
||||||
}
|
|
||||||
var length8 = input.length * 8
|
|
||||||
for (i = 0; i < length8; i += 8) {
|
|
||||||
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the MD5 of a raw string
|
|
||||||
*
|
|
||||||
* @param {string} s Input string
|
|
||||||
* @returns {string} Raw MD5 string
|
|
||||||
*/
|
|
||||||
function rstrMD5(s) {
|
|
||||||
return binl2rstr(binlMD5(rstr2binl(s), s.length * 8))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the HMAC-MD5 of a key and some data (raw strings)
|
|
||||||
*
|
|
||||||
* @param {string} key HMAC key
|
|
||||||
* @param {string} data Raw input string
|
|
||||||
* @returns {string} Raw MD5 string
|
|
||||||
*/
|
|
||||||
function rstrHMACMD5(key, data) {
|
|
||||||
var i
|
|
||||||
var bkey = rstr2binl(key)
|
|
||||||
var ipad = []
|
|
||||||
var opad = []
|
|
||||||
var hash
|
|
||||||
ipad[15] = opad[15] = undefined
|
|
||||||
if (bkey.length > 16) {
|
|
||||||
bkey = binlMD5(bkey, key.length * 8)
|
|
||||||
}
|
|
||||||
for (i = 0; i < 16; i += 1) {
|
|
||||||
ipad[i] = bkey[i] ^ 0x36363636
|
|
||||||
opad[i] = bkey[i] ^ 0x5c5c5c5c
|
|
||||||
}
|
|
||||||
hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8)
|
|
||||||
return binl2rstr(binlMD5(opad.concat(hash), 512 + 128))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a raw string to a hex string
|
|
||||||
*
|
|
||||||
* @param {string} input Raw input string
|
|
||||||
* @returns {string} Hex encoded string
|
|
||||||
*/
|
|
||||||
function rstr2hex(input) {
|
|
||||||
var hexTab = '0123456789abcdef'
|
|
||||||
var output = ''
|
|
||||||
var x
|
|
||||||
var i
|
|
||||||
for (i = 0; i < input.length; i += 1) {
|
|
||||||
x = input.charCodeAt(i)
|
|
||||||
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f)
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode a string as UTF-8
|
|
||||||
*
|
|
||||||
* @param {string} input Input string
|
|
||||||
* @returns {string} UTF8 string
|
|
||||||
*/
|
|
||||||
function str2rstrUTF8(input) {
|
|
||||||
return unescape(encodeURIComponent(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes input string as raw MD5 string
|
|
||||||
*
|
|
||||||
* @param {string} s Input string
|
|
||||||
* @returns {string} Raw MD5 string
|
|
||||||
*/
|
|
||||||
function rawMD5(s) {
|
|
||||||
return rstrMD5(str2rstrUTF8(s))
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Encodes input string as Hex encoded string
|
|
||||||
*
|
|
||||||
* @param {string} s Input string
|
|
||||||
* @returns {string} Hex encoded string
|
|
||||||
*/
|
|
||||||
function hexMD5(s) {
|
|
||||||
return rstr2hex(rawMD5(s))
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Calculates the raw HMAC-MD5 for the given key and data
|
|
||||||
*
|
|
||||||
* @param {string} k HMAC key
|
|
||||||
* @param {string} d Input string
|
|
||||||
* @returns {string} Raw MD5 string
|
|
||||||
*/
|
|
||||||
function rawHMACMD5(k, d) {
|
|
||||||
return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d))
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Calculates the Hex encoded HMAC-MD5 for the given key and data
|
|
||||||
*
|
|
||||||
* @param {string} k HMAC key
|
|
||||||
* @param {string} d Input string
|
|
||||||
* @returns {string} Raw MD5 string
|
|
||||||
*/
|
|
||||||
function hexHMACMD5(k, d) {
|
|
||||||
return rstr2hex(rawHMACMD5(k, d))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates MD5 value for a given string.
|
|
||||||
* If a key is provided, calculates the HMAC-MD5 value.
|
|
||||||
* Returns a Hex encoded string unless the raw argument is given.
|
|
||||||
*
|
|
||||||
* @param {string} string Input string
|
|
||||||
* @param {string} [key] HMAC key
|
|
||||||
* @param {boolean} [raw] Raw output switch
|
|
||||||
* @returns {string} MD5 output
|
|
||||||
*/
|
|
||||||
function md5(string, key, raw) {
|
|
||||||
if (!key) {
|
|
||||||
if (!raw) {
|
|
||||||
return hexMD5(string)
|
|
||||||
}
|
|
||||||
return rawMD5(string)
|
|
||||||
}
|
|
||||||
if (!raw) {
|
|
||||||
return hexHMACMD5(key, string)
|
|
||||||
}
|
|
||||||
return rawHMACMD5(key, string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
define(function() {
|
|
||||||
return md5
|
|
||||||
})
|
|
||||||
} else if (typeof module === 'object' && module.exports) {
|
|
||||||
module.exports = md5
|
|
||||||
} else {
|
|
||||||
$.md5 = md5
|
|
||||||
}
|
|
||||||
})(this)
|
|
@@ -20,7 +20,6 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="{{urlpath}}/bwrs_static/md5.js"></script>
|
|
||||||
<script src="{{urlpath}}/bwrs_static/identicon.js"></script>
|
<script src="{{urlpath}}/bwrs_static/identicon.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function reload() { window.location.reload(); }
|
function reload() { window.location.reload(); }
|
||||||
@@ -28,8 +27,17 @@
|
|||||||
text && alert(text);
|
text && alert(text);
|
||||||
reload_page && reload();
|
reload_page && reload();
|
||||||
}
|
}
|
||||||
function identicon(email) {
|
async function sha256(message) {
|
||||||
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
||||||
|
const msgUint8 = new TextEncoder().encode(message);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
async function identicon(email) {
|
||||||
|
const hash = await sha256(email);
|
||||||
|
const data = new Identicon(hash, { size: 48, format: 'svg' });
|
||||||
return "data:image/svg+xml;base64," + data.toString();
|
return "data:image/svg+xml;base64," + data.toString();
|
||||||
}
|
}
|
||||||
function toggleVis(input_id) {
|
function toggleVis(input_id) {
|
||||||
@@ -125,4 +133,4 @@
|
|||||||
<!-- This script needs to be at the bottom, else it will fail! -->
|
<!-- This script needs to be at the bottom, else it will fail! -->
|
||||||
<script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script>
|
<script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
|
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
|
||||||
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
|
||||||
|
|
||||||
<h3>Version</h3>
|
<h3>Versions</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@@ -35,6 +35,10 @@
|
|||||||
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
|
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
<dt class="col-sm-5">Database</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span><b>{{diagnostics.db_type}}:</b> {{diagnostics.db_version}}</span>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,35 +50,65 @@
|
|||||||
<dt class="col-sm-5">Running within Docker</dt>
|
<dt class="col-sm-5">Running within Docker</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{#if diagnostics.running_within_docker}}
|
{{#if diagnostics.running_within_docker}}
|
||||||
<span id="running-docker" class="d-block"><b>Yes</b></span>
|
<span class="d-block"><b>Yes</b></span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless diagnostics.running_within_docker}}
|
{{#unless diagnostics.running_within_docker}}
|
||||||
<span id="running-docker" class="d-block"><b>No</b></span>
|
<span class="d-block"><b>No</b></span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">Uses a proxy</dt>
|
<dt class="col-sm-5">Uses a reverse proxy</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{#if diagnostics.uses_proxy}}
|
{{#if diagnostics.ip_header_exists}}
|
||||||
<span id="running-docker" class="d-block"><b>Yes</b></span>
|
<span class="d-block" title="IP Header found."><b>Yes</b></span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless diagnostics.uses_proxy}}
|
{{#unless diagnostics.ip_header_exists}}
|
||||||
<span id="running-docker" class="d-block"><b>No</b></span>
|
<span class="d-block" title="No IP Header found."><b>No</b></span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
|
{{!-- Only show this if the IP Header Exists --}}
|
||||||
|
{{#if diagnostics.ip_header_exists}}
|
||||||
|
<dt class="col-sm-5">IP header
|
||||||
|
{{#if diagnostics.ip_header_match}}
|
||||||
|
<span class="badge badge-success" title="IP_HEADER config seems to be valid.">Match</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#unless diagnostics.ip_header_match}}
|
||||||
|
<span class="badge badge-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span>
|
||||||
|
{{/unless}}
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
{{#if diagnostics.ip_header_match}}
|
||||||
|
<span class="d-block"><b>Config/Server:</b> {{ diagnostics.ip_header_name }}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#unless diagnostics.ip_header_match}}
|
||||||
|
<span class="d-block"><b>Config:</b> {{ diagnostics.ip_header_config }}</span>
|
||||||
|
<span class="d-block"><b>Server:</b> {{ diagnostics.ip_header_name }}</span>
|
||||||
|
{{/unless}}
|
||||||
|
</dd>
|
||||||
|
{{/if}}
|
||||||
|
{{!-- End if IP Header Exists --}}
|
||||||
<dt class="col-sm-5">Internet access
|
<dt class="col-sm-5">Internet access
|
||||||
{{#if diagnostics.has_http_access}}
|
{{#if diagnostics.has_http_access}}
|
||||||
<span class="badge badge-success" id="internet-success" title="We have internet access!">Ok</span>
|
<span class="badge badge-success" title="We have internet access!">Ok</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless diagnostics.has_http_access}}
|
{{#unless diagnostics.has_http_access}}
|
||||||
<span class="badge badge-danger" id="internet-warning" title="There seems to be no internet access. Please fix.">Error</span>
|
<span class="badge badge-danger" title="There seems to be no internet access. Please fix.">Error</span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="col-sm-7">
|
<dd class="col-sm-7">
|
||||||
{{#if diagnostics.has_http_access}}
|
{{#if diagnostics.has_http_access}}
|
||||||
<span id="running-docker" class="d-block"><b>Yes</b></span>
|
<span class="d-block"><b>Yes</b></span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless diagnostics.has_http_access}}
|
{{#unless diagnostics.has_http_access}}
|
||||||
<span id="running-docker" class="d-block"><b>No</b></span>
|
<span class="d-block"><b>No</b></span>
|
||||||
|
{{/unless}}
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-5">Internet access via a proxy</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
{{#if diagnostics.uses_proxy}}
|
||||||
|
<span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span>
|
||||||
|
{{/if}}
|
||||||
|
{{#unless diagnostics.uses_proxy}}
|
||||||
|
<span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
<dt class="col-sm-5">DNS (github.com)
|
<dt class="col-sm-5">DNS (github.com)
|
||||||
@@ -263,16 +297,18 @@
|
|||||||
supportString += "* Bitwarden_rs version: v{{ version }}\n";
|
supportString += "* Bitwarden_rs version: v{{ version }}\n";
|
||||||
supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n";
|
supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n";
|
||||||
supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n";
|
supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n";
|
||||||
|
supportString += "* Uses a reverse proxy: {{ diagnostics.ip_header_exists }}\n";
|
||||||
|
{{#if diagnostics.ip_header_exists}}
|
||||||
|
supportString += "* IP Header check: {{ diagnostics.ip_header_match }} ({{ diagnostics.ip_header_name }})\n";
|
||||||
|
{{/if}}
|
||||||
supportString += "* Internet access: {{ diagnostics.has_http_access }}\n";
|
supportString += "* Internet access: {{ diagnostics.has_http_access }}\n";
|
||||||
supportString += "* Uses a proxy: {{ diagnostics.uses_proxy }}\n";
|
supportString += "* Internet access via a proxy: {{ diagnostics.uses_proxy }}\n";
|
||||||
supportString += "* DNS Check: " + dnsCheck + "\n";
|
supportString += "* DNS Check: " + dnsCheck + "\n";
|
||||||
supportString += "* Time Check: " + timeCheck + "\n";
|
supportString += "* Time Check: " + timeCheck + "\n";
|
||||||
supportString += "* Domain Configuration Check: " + domainCheck + "\n";
|
supportString += "* Domain Configuration Check: " + domainCheck + "\n";
|
||||||
supportString += "* HTTPS Check: " + httpsCheck + "\n";
|
supportString += "* HTTPS Check: " + httpsCheck + "\n";
|
||||||
supportString += "* Database type: {{ diagnostics.db_type }}\n";
|
supportString += "* Database type: {{ diagnostics.db_type }}\n";
|
||||||
{{#case diagnostics.db_type "MySQL" "PostgreSQL"}}
|
supportString += "* Database version: {{ diagnostics.db_version }}\n";
|
||||||
supportString += "* Database version: [PLEASE PROVIDE DATABASE VERSION]\n";
|
|
||||||
{{/case}}
|
|
||||||
supportString += "* Clients used: \n";
|
supportString += "* Clients used: \n";
|
||||||
supportString += "* Reverse proxy and version: \n";
|
supportString += "* Reverse proxy and version: \n";
|
||||||
supportString += "* Other relevant information: \n";
|
supportString += "* Other relevant information: \n";
|
||||||
|
@@ -73,9 +73,11 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("img.identicon").forEach(function (e, i) {
|
(async () => {
|
||||||
e.src = identicon(e.dataset.src);
|
for (let e of document.querySelectorAll("img.identicon")) {
|
||||||
});
|
e.src = await identicon(e.dataset.src);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function(event) {
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
$('#orgs-table').DataTable({
|
$('#orgs-table').DataTable({
|
||||||
@@ -87,4 +89,4 @@
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@@ -206,9 +206,11 @@
|
|||||||
"3": { "name": "Manager", "color": "green" },
|
"3": { "name": "Manager", "color": "green" },
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll("img.identicon").forEach(function (e, i) {
|
(async () => {
|
||||||
e.src = identicon(e.dataset.src);
|
for (let e of document.querySelectorAll("img.identicon")) {
|
||||||
});
|
e.src = await identicon(e.dataset.src);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
|
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
|
||||||
let orgtype = OrgTypes[e.dataset.orgtype];
|
let orgtype = OrgTypes[e.dataset.orgtype];
|
||||||
@@ -288,4 +290,4 @@
|
|||||||
"Error updating organization type of the user", data);
|
"Error updating organization type of the user", data);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
59
src/util.rs
59
src/util.rs
@@ -23,7 +23,7 @@ impl Fairing for AppHeaders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_response(&self, _req: &Request, res: &mut Response) {
|
fn on_response(&self, _req: &Request, res: &mut Response) {
|
||||||
res.set_raw_header("Feature-Policy", "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; sync-xhr 'self' https://haveibeenpwned.com https://twofactorauth.org; usb 'none'; vr 'none'");
|
res.set_raw_header("Feature-Policy", "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; sync-xhr 'self' https://haveibeenpwned.com https://2fa.directory; usb 'none'; vr 'none'");
|
||||||
res.set_raw_header("Referrer-Policy", "same-origin");
|
res.set_raw_header("Referrer-Policy", "same-origin");
|
||||||
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
|
||||||
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
res.set_raw_header("X-Content-Type-Options", "nosniff");
|
||||||
@@ -38,9 +38,9 @@ impl Fairing for AppHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CORS();
|
pub struct Cors();
|
||||||
|
|
||||||
impl CORS {
|
impl Cors {
|
||||||
fn get_header(headers: &HeaderMap, name: &str) -> String {
|
fn get_header(headers: &HeaderMap, name: &str) -> String {
|
||||||
match headers.get_one(name) {
|
match headers.get_one(name) {
|
||||||
Some(h) => h.to_string(),
|
Some(h) => h.to_string(),
|
||||||
@@ -48,18 +48,24 @@ impl CORS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_url(url: String) -> String {
|
// Check a request's `Origin` header against the list of allowed origins.
|
||||||
match url.as_ref() {
|
// If a match exists, return it. Otherwise, return None.
|
||||||
"file://" => "*".to_string(),
|
fn get_allowed_origin(headers: &HeaderMap) -> Option<String> {
|
||||||
_ => url,
|
let origin = Cors::get_header(headers, "Origin");
|
||||||
|
let domain_origin = CONFIG.domain_origin();
|
||||||
|
let safari_extension_origin = "file://";
|
||||||
|
if origin == domain_origin || origin == safari_extension_origin {
|
||||||
|
Some(origin)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fairing for CORS {
|
impl Fairing for Cors {
|
||||||
fn info(&self) -> Info {
|
fn info(&self) -> Info {
|
||||||
Info {
|
Info {
|
||||||
name: "CORS",
|
name: "Cors",
|
||||||
kind: Kind::Response,
|
kind: Kind::Response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,14 +73,14 @@ impl Fairing for CORS {
|
|||||||
fn on_response(&self, request: &Request, response: &mut Response) {
|
fn on_response(&self, request: &Request, response: &mut Response) {
|
||||||
let req_headers = request.headers();
|
let req_headers = request.headers();
|
||||||
|
|
||||||
// We need to explicitly get the Origin header for Access-Control-Allow-Origin
|
if let Some(origin) = Cors::get_allowed_origin(req_headers) {
|
||||||
let req_allow_origin = CORS::valid_url(CORS::get_header(req_headers, "Origin"));
|
response.set_header(Header::new("Access-Control-Allow-Origin", origin));
|
||||||
|
}
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
|
|
||||||
|
|
||||||
|
// Preflight request
|
||||||
if request.method() == Method::Options {
|
if request.method() == Method::Options {
|
||||||
let req_allow_headers = CORS::get_header(req_headers, "Access-Control-Request-Headers");
|
let req_allow_headers = Cors::get_header(req_headers, "Access-Control-Request-Headers");
|
||||||
let req_allow_method = CORS::get_header(req_headers, "Access-Control-Request-Method");
|
let req_allow_method = Cors::get_header(req_headers, "Access-Control-Request-Method");
|
||||||
|
|
||||||
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_method));
|
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_method));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Headers", req_allow_headers));
|
response.set_header(Header::new("Access-Control-Allow-Headers", req_allow_headers));
|
||||||
@@ -86,17 +92,21 @@ impl Fairing for CORS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Cached<R>(R, &'static str);
|
pub struct Cached<R>(R, String);
|
||||||
|
|
||||||
impl<R> Cached<R> {
|
impl<R> Cached<R> {
|
||||||
pub const fn long(r: R) -> Cached<R> {
|
pub fn long(r: R) -> Cached<R> {
|
||||||
// 7 days
|
// 7 days
|
||||||
Self(r, "public, max-age=604800")
|
Self::ttl(r, 604800)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn short(r: R) -> Cached<R> {
|
pub fn short(r: R) -> Cached<R> {
|
||||||
// 10 minutes
|
// 10 minutes
|
||||||
Self(r, "public, max-age=600")
|
Self(r, String::from("public, max-age=600"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ttl(r: R, ttl: u64) -> Cached<R> {
|
||||||
|
Self(r, format!("public, immutable, max-age={}", ttl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +368,15 @@ pub fn format_naive_datetime_local(dt: &NaiveDateTime, fmt: &str) -> String {
|
|||||||
format_datetime_local(&Local.from_utc_datetime(dt), fmt)
|
format_datetime_local(&Local.from_utc_datetime(dt), fmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Deployment environment methods
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Returns true if the program is running in Docker or Podman.
|
||||||
|
pub fn is_running_in_docker() -> bool {
|
||||||
|
Path::new("/.dockerenv").exists() || Path::new("/run/.containerenv").exists()
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Deserialization methods
|
// Deserialization methods
|
||||||
//
|
//
|
||||||
|
Reference in New Issue
Block a user