Compare commits

...

66 Commits

Author SHA1 Message Date
Daniel García
fd27759a95 Merge pull request #1546 from RealOrangeOne/clippy-run
Run Clippy
2021-03-28 16:04:09 +02:00
Daniel García
01d8056c73 Merge pull request #1545 from RealOrangeOne/icon-client-cache
Client caching
2021-03-28 16:03:16 +02:00
Jake Howard
81fa33ebb5 Remove unnecessary reference 2021-03-28 10:59:49 +01:00
Jake Howard
e8aa3bc066 Merge branch 'master' into clippy-run 2021-03-28 10:51:25 +01:00
Jake Howard
0bf0125e82 Reverse negation on ordering
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
2021-03-28 10:49:29 +01:00
Jake Howard
6209e778e5 Icons should always be cached using full TTL 2021-03-28 10:39:12 +01:00
Daniel García
5323283f98 Merge pull request #1548 from BlackDex/admin-interface
Updated diagnostics page
2021-03-28 01:31:38 +01:00
BlackDex
57e17d0648 Updated diagnostics page
- Added reverse proxy check
- Better deffinition of internet proxy
- Added SQL Server version detection
2021-03-28 00:10:01 +01:00
Jake Howard
da55d5ec70 Also run actions CI on pull request
`push` only counts for pushes to branches on the repo, not forks
2021-03-27 15:21:00 +00:00
Jake Howard
828a060698 Run clippy on CI 2021-03-27 15:10:00 +00:00
Jake Howard
3e5971b9db Remove unnecessary result return types 2021-03-27 15:07:26 +00:00
Jake Howard
47c2625d38 Prevent clippy complaining at method
It's not incorrectly wrapped. We care about the return type being `Option`.
2021-03-27 14:36:50 +00:00
Jake Howard
49af9cf4f5 Correctly camelCase acronyms
https://rust-lang.github.io/rust-clippy/master/index.html#upper_case_acronyms
2021-03-27 14:26:32 +00:00
Jake Howard
6b1daeba05 Implement From over Into
https://rust-lang.github.io/rust-clippy/master/index.html#from_over_into
2021-03-27 14:19:57 +00:00
Jake Howard
9f1240d8d9 Only construct JSON object if it's useful 2021-03-27 14:03:46 +00:00
Jake Howard
a8138be69b Use if let more 2021-03-27 14:03:31 +00:00
Jake Howard
ea57dc3bc9 Use matches macro 2021-03-27 14:03:07 +00:00
Jake Howard
131348a49f Add immutable caching for vault assets
The URLs are cachebusted, so updates will still be applied cleanly and immediately
2021-03-27 13:37:56 +00:00
Jake Howard
b22564cb00 Cache icons on the client
This should make the vault pages load much faster, and massively reduce the number of requests.
2021-03-27 13:30:40 +00:00
Daniel García
16eb0a56f9 Exclude vendored scripts from Github language statistics 2021-03-25 21:39:34 +01:00
Daniel García
3e4ff47a38 Update dependencies, particularly openssl to 1.1.1k 2021-03-25 20:05:20 +01:00
Daniel García
8ea01a67f6 Merge pull request #1529 from mprasil/more-generic-send-error-messages
Return generic message when Send not available
2021-03-25 19:56:24 +01:00
Miro Prasil
aa5cc642e1 Use constant for the "inaccessible" error message 2021-03-25 11:40:32 +00:00
Daniel García
a121cb6f00 Merge pull request #1530 from jjlin/global-domains
Sync global_domains.json
2021-03-23 23:48:20 +01:00
Daniel García
60164182ae Fix alpine armv7 build
Reference: https://github.com/messense/rust-musl-cross/pull/34
2021-03-23 23:47:12 +01:00
Jeremy Lin
f842a80cdb Sync global_domains.json to bitwarden/server@455e4b2 (ProtonMail/ProtonVPN) 2021-03-23 11:30:00 -07:00
Miro Prasil
4b6a574ee0 Return generic message when Send not available
This should help avoid leaking information about (non)existence of Send
and be more in line with what official server returns.
2021-03-23 13:39:09 +00:00
Daniel García
f9ebb780f9 Update dependencies 2021-03-22 20:00:57 +01:00
Daniel García
1fc6c30652 Send deletion thread and updated users revision 2021-03-22 19:57:35 +01:00
Daniel García
46a1a013cd Update user revision date with sends 2021-03-22 19:05:15 +01:00
Daniel García
551810c486 Fix updating file send 2021-03-17 19:39:48 +01:00
Daniel García
b987ba506d Merge pull request #1493 from jjlin/send
Add support for the Disable Send policy
2021-03-16 18:13:55 +01:00
Daniel García
84810f2bb2 Remove unnecessary fields from send access 2021-03-16 18:11:25 +01:00
Jeremy Lin
424d666a50 Add support for the Disable Send policy
Upstream refs:

* https://github.com/bitwarden/server/pull/1130
* https://bitwarden.com/help/article/policies/#disable-send
2021-03-16 02:07:45 -07:00
Daniel García
a71359f647 Merge pull request #1469 from jjlin/cors
CORS fixes
2021-03-15 16:57:00 +01:00
Daniel García
d93c344176 Merge branch 'master' into cors 2021-03-15 16:49:12 +01:00
Daniel García
b9c3213b90 Merge pull request #1487 from jjlin/send
Send access check fixes
2021-03-15 16:47:14 +01:00
Daniel García
95e24ffc51 rename send key -> akey 2021-03-15 16:42:20 +01:00
Jeremy Lin
00d56d7295 Send access check fixes
Adjust checks for max access count, expiration date, and deletion date.
The date checks aren't that important, but the access count check
currently allows one more access than it should.
2021-03-14 23:20:49 -07:00
Daniel García
7436b454db Update web vault to 2.19.0 2021-03-14 23:36:49 +01:00
Daniel García
8da5b99482 Send API 2021-03-14 23:35:55 +01:00
Daniel García
2969e87b52 Add separate host-only fromrequest handler 2021-03-14 23:24:47 +01:00
Daniel García
ce62e898c3 Remove debug impl from database structs
This is only implemented for the database specific structs, which is not what we want
2021-03-13 22:04:04 +01:00
Daniel García
431462d839 Update dependencies and enable serde integration for chrono 2021-03-13 22:02:11 +01:00
Jeremy Lin
7d0e234b34 CORS fixes
* The Safari extension apparently now uses the origin `file://` and expects
  that to be returned (see bitwarden/browser#1311, bitwarden/server#800).

* The `Access-Control-Allow-Origin` header was reflecting the value of the
  `Origin` header without checking whether the origin was actually allowed.
  This effectively allows any origin to interact with the server, which
  defeats the purpose of CORS.
2021-03-07 00:35:08 -08:00
Daniel García
dad1b1bee9 Updated dependencies 2021-03-06 22:04:01 +01:00
Daniel García
9312cebee3 Merge pull request #1463 from std2main/patch-2
Add a dot in find command.
2021-03-06 00:04:12 +01:00
std2main
cdf5b6ec2d Add a dot in find command.
Add a dot indicting current directory to search by find.

find in mac won't work without the dot
2021-03-05 15:49:45 -05:00
Mathijs van Veluw
ce99fc8f95 Merge pull request #1460 from jjlin/invitation-org-name
Fix custom org name in invitation confirmation email
2021-03-04 08:15:34 +01:00
Jeremy Lin
a75d050001 Fix custom org name in invitation confirmation email
The org name in the invitation email was made customizable in 8867626, but
the org name is still hardcoded as "bitwarden_rs" in the confirmation email.
2021-03-03 23:03:55 -08:00
Daniel García
75cfd10f11 Merge pull request #1444 from jjlin/remove-md5
Remove `md5.js` dependency
2021-02-28 18:23:27 +01:00
Daniel García
9859ba6339 Merge pull request #1443 from jjlin/data-folder
Check for data folder on startup
2021-02-28 18:22:46 +01:00
Jeremy Lin
513056f711 Check for data folder on startup
Currently, when starting up for the first time (running standalone, outside
of Docker), bitwarden_rs panics when the `openssl` tool isn't able to create
`data/rsa_key.pem` due to the `data` dir not existing. Instead, print a more
helpful error message telling the user to create the directory.
2021-02-28 01:45:05 -08:00
Mathijs van Veluw
ebe334fcc7 Merge pull request #1447 from jjlin/issue-templates
Allow only bug report issues
2021-02-28 08:32:04 +01:00
Jeremy Lin
0eec12472e Allow only bug report issues
Remove templates for other issue types, directing them to the forum instead.
2021-02-27 22:13:51 -08:00
Jeremy Lin
39106d440a Remove md5.js dependency
Switch to the built-in WebCrypto APIs for computing identicon hashes.
2021-02-26 21:48:01 -08:00
Daniel García
9117095764 Update dependencies and web vault 2021-02-24 20:30:19 +01:00
Daniel García
099bba950c Merge pull request #1432 from jjlin/2fa
Change `twofactorauth.org` to `2fa.directory`
2021-02-24 20:05:57 +01:00
Jeremy Lin
e37ff60617 Change twofactorauth.org to 2fa.directory
The `twofactorauth.org` has apparently been sold to some company for
marketing purposes.
2021-02-23 18:51:07 -08:00
Daniel García
5b14608041 Update web vault to have better error messages when not using HTTPS 2021-02-20 19:13:20 +01:00
Daniel García
ad92692bab Merge pull request #1413 from paolobarbolini/email-clones
Remove unnecessary allocations
2021-02-20 17:58:12 +01:00
Paolo Barbolini
d956d42903 Remove unnecessary allocations 2021-02-19 20:17:18 +01:00
Daniel García
d69be7d03a Merge pull request #1389 from jjlin/alpine
Update Alpine base images to 3.13
2021-02-15 20:58:13 +01:00
Jeremy Lin
f82de8d00d Update Alpine base images to 3.13 2021-02-14 15:18:47 -08:00
Daniel García
c836f88ff2 Remove soup and use a newer html5ever directly 2021-02-07 22:28:02 +01:00
Daniel García
8b660ae090 Swap structopt for a simpler alternative 2021-02-07 20:10:40 +01:00
70 changed files with 1856 additions and 1380 deletions

View File

@@ -28,6 +28,7 @@
# RSA_KEY_FILENAME=data/rsa_key
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments
# SENDS_FOLDER=data/sends
## Templates data folder, by default uses embedded templates
## Check source code to see the format

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Ignore vendored scripts in GitHub stats
src/static/* linguist-vendored

View File

@@ -1,51 +1,66 @@
---
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: ''
labels: ''
assignees: ''
---
<!--
# ###
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.
See: https://github.com/dani-garcia/bitwarden_rs/issues/1180
# ###
# ###
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.
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.
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,
such as names, global IP/DNS addresses and especially passwords, if necessary.
Remember to hide/redact personal or confidential information,
such as passwords, IP addresses, and DNS names as appropriate.
-->
### Subject of the issue
<!-- Describe your issue here.-->
<!-- Describe your issue here. -->
### Your 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 -->
### Deployment environment
<!--
=========================================================================================
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:
* Clients used: <!-- if applicable -->
* Clients used: <!-- web vault, desktop, Android, iOS, etc. (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
<!-- Tell us how to reproduce this issue. What parameters did you set (differently from the defaults)
and how did you start bitwarden_rs? -->
### Expected behaviour
<!-- Tell us what should happen -->
<!-- Tell us what you expected to happen -->
### Actual behaviour
<!-- Tell us what happens instead -->
<!-- Tell us what actually happened -->
### Relevant logs
<!-- Share some logfiles, screenshots or output of relevant programs with us. -->
### Troubleshooting data
<!-- Share any log files, screenshots, or other relevant troubleshooting data -->

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -10,6 +10,15 @@ on:
- "docker/**"
- "hooks/**"
- "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:
build:
@@ -82,10 +91,11 @@ jobs:
with:
profile: minimal
target: ${{ matrix.target-triple }}
components: clippy
# 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 }}`'
uses: actions-rs/cargo@v1
with:
@@ -94,6 +104,15 @@ jobs:
# 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
- name: '`cargo build --release --features ${{ matrix.features }} --target ${{ matrix.target-triple }}`'
uses: actions-rs/cargo@v1

803
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,7 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
rocket_contrib = "0.5.0-dev"
# HTTP client
reqwest = { version = "0.11.0", features = ["blocking", "json"] }
reqwest = { version = "0.11.2", features = ["blocking", "json"] }
# multipart/form-data support
multipart = { version = "0.17.1", features = ["server"], default-features = false }
@@ -47,19 +47,19 @@ rmpv = "0.4.7"
chashmap = "2.2.2"
# A generic serialization/deserialization framework
serde = { version = "1.0.123", features = ["derive"] }
serde_json = "1.0.62"
serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
# Logging
log = "0.4.14"
fern = { version = "0.6.0", features = ["syslog-4"] }
# 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"
# 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
rand = "0.8.3"
@@ -69,9 +69,9 @@ ring = "0.16.20"
uuid = { version = "0.8.2", features = ["v4"] }
# Date and time libraries
chrono = "0.4.19"
chrono = { version = "0.4.19", features = ["serde"] }
chrono-tz = "0.5.3"
time = "0.2.25"
time = "0.2.26"
# TOTP library
oath = "0.10.2"
@@ -86,46 +86,47 @@ jsonwebtoken = "7.2.0"
u2f = "0.2.0"
# 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
dotenv = { version = "0.15.0", default-features = false }
# Lazy initialization
once_cell = "1.5.2"
once_cell = "1.7.2"
# Numerical libraries
num-traits = "0.2.14"
num-derive = "0.3.3"
# Email libraries
lettre = { version = "0.10.0-alpha.5", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
newline-converter = "0.1.0"
lettre = { version = "0.10.0-beta.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false }
newline-converter = "0.2.0"
# Template library
handlebars = { version = "3.5.2", features = ["dir_source"] }
handlebars = { version = "3.5.3", features = ["dir_source"] }
# For favicon extraction from main website
soup = "0.5.0"
regex = { version = "1.4.3", features = ["std", "perf"], default-features = false }
html5ever = "0.25.1"
markup5ever_rcdom = "0.1.0"
regex = { version = "1.4.5", features = ["std", "perf"], default-features = false }
data-url = "0.1.0"
# Used by U2F, JWT and Postgres
openssl = "0.10.32"
openssl = "0.10.33"
# URL encoding library
percent-encoding = "2.1.0"
# Punycode conversion
idna = "0.2.1"
idna = "0.2.2"
# CLI argument parsing
structopt = "0.3.21"
pico-args = "0.4.0"
# Logging panics to logfile instead stderr only
backtrace = "0.3.56"
# Macro ident concatenation
paste = "1.0.4"
paste = "1.0.5"
[patch.crates-io]
# Use newest ring

View File

@@ -1,15 +1,15 @@
# This file was generated using a Jinja2 template.
# 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 "amd64" in target_file %}
{% set build_stage_base_image = "clux/muslrust:nightly-2021-01-25" %}
{% set runtime_stage_base_image = "alpine:3.12" %}
{% set build_stage_base_image = "clux/muslrust:nightly-2021-02-22" %}
{% set runtime_stage_base_image = "alpine:3.13" %}
{% set package_arch_target = "x86_64-unknown-linux-musl" %}
{% elif "armv7" in target_file %}
{% 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" %}
{% endif %}
{% elif "amd64" in target_file %}
@@ -44,8 +44,8 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
{% set vault_version = "2.18.1b" %}
{% set vault_image_digest = "sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb" %}
{% set vault_version = "2.19.0" %}
{% set vault_image_digest = "sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4" %}
# The web-vault digest specifies a particular web-vault build on Docker Hub.
# 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
@@ -93,6 +93,9 @@ RUN rustup set profile minimal
{% if "alpine" in target_file %}
ENV USER "root"
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 %}
# 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

View File

@@ -1,4 +1,4 @@
OBJECTS := $(shell find -mindepth 2 -name 'Dockerfile*')
OBJECTS := $(shell find ./ -mindepth 2 -name 'Dockerfile*')
all: $(OBJECTS)

View File

@@ -14,18 +14,18 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.48 as build
FROM rust:1.50 as build
# Debian-based builds support multidb
ARG DB=sqlite,mysql,postgresql

View File

@@ -14,18 +14,18 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## 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.
ARG DB=sqlite,postgresql
@@ -70,7 +70,7 @@ RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM alpine:3.12
FROM alpine:3.13
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80

View File

@@ -14,18 +14,18 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.48 as build
FROM rust:1.50 as build
# Debian-based builds support multidb
ARG DB=sqlite,mysql,postgresql

View File

@@ -14,18 +14,18 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.48 as build
FROM rust:1.50 as build
# Debian-based builds support multidb
ARG DB=sqlite,mysql,postgresql

View File

@@ -14,18 +14,18 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## BUILD IMAGE ##########################
FROM rust:1.48 as build
FROM rust:1.50 as build
# Debian-based builds support multidb
ARG DB=sqlite,mysql,postgresql

View File

@@ -14,15 +14,15 @@
# - 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.
# - From the command line:
# $ docker pull bitwardenrs/web-vault:v2.18.1b
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.1b
# [bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb]
# $ docker pull bitwardenrs/web-vault:v2.19.0
# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0
# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4]
#
# - Conversely, to get the tag name from the digest:
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb
# [bitwardenrs/web-vault:v2.18.1b]
# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4
# [bitwardenrs/web-vault:v2.19.0]
#
FROM bitwardenrs/web-vault@sha256:345a509dd5482343458b672dcd69203836ffac2e5181a1c99826d9695b9cb1eb as vault
FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault
########################## BUILD IMAGE ##########################
FROM messense/rust-musl-cross:armv7-musleabihf as build
@@ -38,6 +38,7 @@ RUN rustup set profile minimal
ENV USER "root"
ENV RUSTFLAGS='-C link-arg=-s'
ENV CFLAGS_armv7_unknown_linux_musleabihf="-mfpu=vfpv3-d16"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
@@ -71,7 +72,7 @@ RUN musl-strip target/armv7-unknown-linux-musleabihf/release/bitwarden_rs
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# 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_PORT=80

View File

@@ -0,0 +1 @@
DROP TABLE sends;

View 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
);

View File

@@ -0,0 +1 @@
DROP TABLE sends;

View 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
);

View File

@@ -0,0 +1 @@
ALTER TABLE sends RENAME COLUMN key TO akey;

View File

@@ -0,0 +1 @@
DROP TABLE sends;

View 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
);

View File

@@ -0,0 +1 @@
ALTER TABLE sends RENAME COLUMN key TO akey;

View File

@@ -1 +1 @@
nightly-2021-01-25
nightly-2021-02-22

View File

@@ -13,13 +13,13 @@ use rocket::{
use rocket_contrib::json::Json;
use crate::{
api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
api::{ApiResult, EmptyResult, NumberOrString},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
config::ConfigBuilder,
db::{backup_database, models::*, DbConn, DbConnType},
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
error::{Error, MapResult},
mail,
util::{format_naive_datetime_local, get_display_size},
util::{format_naive_datetime_local, get_display_size, is_running_in_docker},
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
/// (see https://tools.ietf.org/html/rfc2616#section-14.30).
fn admin_url(referer: Referer) -> String {
@@ -291,17 +312,17 @@ fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
}
#[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));
Ok(Redirect::to(admin_url(referer)))
Redirect::to(admin_url(referer))
}
#[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_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")]
@@ -475,7 +496,7 @@ fn has_http_access() -> bool {
}
#[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 chrono::prelude::*;
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)?;
// 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 uses_proxy = 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())
};
let ip_header_name = match &ip_header.0 {
Some(h) => h,
_ => ""
};
let diagnostics_json = json!({
"dns_resolved": dns_resolved,
"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,
"running_within_docker": running_within_docker,
"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,
"db_type": *DB_TYPE,
"db_version": get_sql_server_version(&conn),
"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
});
@@ -548,9 +579,9 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> {
}
#[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();
Ok(Json(support_json))
Json(support_json)
}
#[post("/config", data = "<data>")]

View File

@@ -1,5 +1,6 @@
use chrono::Utc;
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::{
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);
} else {
if let Err(e) = mail::send_welcome(&user.email) {
error!("Error sending welcome email: {:#?}", e);
}
} else if let Err(e) = mail::send_welcome(&user.email) {
error!("Error sending welcome email: {:#?}", e);
}
}
@@ -150,8 +149,8 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
}
#[get("/accounts/profile")]
fn profile(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(headers.user.to_json(&conn)))
fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
Json(headers.user.to_json(&conn))
}
#[derive(Deserialize, Debug)]
@@ -612,7 +611,7 @@ struct PreloginData {
}
#[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 (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),
};
Ok(Json(json!({
Json(json!({
"Kdf": kdf_type,
"KdfIterations": kdf_iter
})))
}))
}
#[derive(Deserialize)]
#[allow(non_snake_case)]

View File

@@ -84,7 +84,7 @@ struct SyncData {
}
#[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 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))
.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 {
Value::Null
} 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,
"Folders": folders_json,
"Collections": collections_json,
"Policies": policies_json,
"Ciphers": ciphers_json,
"Domains": domains_json,
"Sends": sends_json,
"Object": "sync"
})))
}))
}
#[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_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))
.collect();
Ok(Json(json!({
Json(json!({
"Data": ciphers_json,
"Object": "list",
"ContinuationToken": null
})))
}))
}
#[get("/ciphers/<uuid>")]
@@ -278,19 +285,10 @@ fn enforce_personal_ownership_policy(
) -> EmptyResult {
if data.OrganizationId.is_none() {
let user_uuid = &headers.user.uuid;
for policy in OrgPolicy::find_by_user(user_uuid, conn) {
if policy.enabled && policy.has_type(OrgPolicyType::PersonalOwnership) {
let org_uuid = &policy.org_uuid;
match UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
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"),
}
}
let policy_type = OrgPolicyType::PersonalOwnership;
if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn) {
err!("Due to an Enterprise Policy, you are restricted from \
saving items to your personal vault.")
}
}
Ok(())

View File

@@ -20,16 +20,16 @@ pub fn routes() -> Vec<rocket::Route> {
}
#[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_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
Ok(Json(json!({
Json(json!({
"Data": folders_json,
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[get("/folders/<uuid>")]

View File

@@ -3,6 +3,9 @@ mod ciphers;
mod folders;
mod organizations;
pub mod two_factor;
mod sends;
pub use sends::start_send_deletion_scheduler;
pub fn routes() -> Vec<Route> {
let mut mod_routes = routes![
@@ -20,6 +23,7 @@ pub fn routes() -> Vec<Route> {
routes.append(&mut folders::routes());
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut sends::routes());
routes.append(&mut mod_routes);
routes
@@ -30,17 +34,18 @@ pub fn routes() -> Vec<Route> {
//
use rocket::Route;
use rocket_contrib::json::Json;
use rocket::response::Response;
use serde_json::Value;
use crate::{
api::{EmptyResult, JsonResult, JsonUpcase},
api::{JsonResult, JsonUpcase},
auth::Headers,
db::DbConn,
error::Error,
};
#[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
let _ = uuid;
@@ -49,11 +54,11 @@ fn clear_device_token(uuid: String) -> EmptyResult {
// 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/Core/Services/Implementations/DeviceService.cs#L37
Ok(())
Response::new()
}
#[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;
// Data has a single string value "PushToken"
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
Ok(Json(json!({
Json(json!({
"Id": headers.device.uuid,
"Name": headers.device.name,
"Type": headers.device.atype,
"Identifier": headers.device.uuid,
"CreationDate": crate::util::format_date(&headers.device.created_at),
})))
}))
}
#[derive(Serialize, Deserialize, Debug)]
@@ -81,11 +86,11 @@ struct GlobalDomain {
const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json");
#[get("/settings/domains")]
fn get_eq_domains(headers: Headers) -> JsonResult {
fn get_eq_domains(headers: Headers) -> Json<Value> {
_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;
use serde_json::from_str;
@@ -102,11 +107,11 @@ fn _get_eq_domains(headers: Headers, no_excluded: bool) -> JsonResult {
globals.retain(|g| !g.Excluded);
}
Ok(Json(json!({
Json(json!({
"EquivalentDomains": equivalent_domains,
"GlobalEquivalentDomains": globals,
"Object": "domains",
})))
}))
}
#[derive(Deserialize, Debug)]

View File

@@ -192,8 +192,8 @@ fn post_organization(
// GET /api/collections?writeOnly=false
#[get("/collections")]
fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(json!({
fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
Json(json!({
"Data":
Collection::find_by_user_uuid(&headers.user.uuid, &conn)
.iter()
@@ -201,12 +201,12 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
.collect::<Value>(),
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[get("/organizations/<org_id>/collections")]
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
Ok(Json(json!({
fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> Json<Value> {
Json(json!({
"Data":
Collection::find_by_organization(&org_id, &conn)
.iter()
@@ -214,7 +214,7 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
.collect::<Value>(),
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[post("/organizations/<org_id>/collections", data = "<data>")]
@@ -441,30 +441,30 @@ struct OrgIdData {
}
#[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_json: Vec<Value> = ciphers
.iter()
.map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn))
.collect();
Ok(Json(json!({
Json(json!({
"Data": ciphers_json,
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[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_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
Ok(Json(json!({
Json(json!({
"Data": users_json,
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[derive(Deserialize)]
@@ -655,7 +655,7 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
}
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 {
org_name = match Organization::find_by_uuid(&org_id, &conn) {
Some(org) => org.name,
@@ -930,15 +930,15 @@ fn post_org_import(
}
#[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_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
Ok(Json(json!({
Json(json!({
"Data": policies_json,
"Object": "list",
"ContinuationToken": null
})))
}))
}
#[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")]
fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
Ok(Json(json!({
fn get_plans(_headers: Headers, _conn: DbConn) -> Json<Value> {
Json(json!({
"Object": "list",
"Data": [
{
@@ -1065,17 +1065,17 @@ fn get_plans(_headers: Headers, _conn: DbConn) -> JsonResult {
}
],
"ContinuationToken": null
})))
}))
}
#[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.
Ok(Json(json!({
Json(json!({
"Object": "list",
"Data": [],
"ContinuationToken": null
})))
}))
}
#[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
} 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) {
let user_org_status = if CONFIG.mail_enabled() {
UserOrgStatus::Invited as i32
} else {
@@ -1157,18 +1157,18 @@ fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Headers, con
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 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 !data.Users.iter().any(|u| u.Email == user_email) {
user_org.delete(&conn)?;
}
}
}
}
}

416
src/api/core/sends.rs Normal file
View 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()))
}

View File

@@ -38,15 +38,15 @@ pub fn routes() -> Vec<Route> {
}
#[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_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_provider).collect();
Ok(Json(json!({
Json(json!({
"Data": twofactors_json,
"Object": "list",
"ContinuationToken": null,
})))
}))
}
#[post("/two-factor/get-recover", data = "<data>")]

View File

@@ -131,12 +131,12 @@ struct RegisterResponseCopy {
pub error_code: Option<NumberOrString>,
}
impl Into<RegisterResponse> for RegisterResponseCopy {
fn into(self) -> RegisterResponse {
impl From<RegisterResponseCopy> for RegisterResponse {
fn from(r: RegisterResponseCopy) -> RegisterResponse {
RegisterResponse {
registration_data: self.registration_data,
version: self.version,
client_data: self.client_data,
registration_data: r.registration_data,
version: r.version,
client_data: r.client_data,
}
}
}

View File

@@ -11,7 +11,6 @@ use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::{blocking::Client, blocking::Response, header, Url};
use rocket::{http::ContentType, http::Cookie, response::Content, Route};
use soup::prelude::*;
use crate::{error::Error, util::Cached, CONFIG};
@@ -52,7 +51,7 @@ fn icon(domain: String) -> Option<Cached<Content<Vec<u8>>>> {
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.
@@ -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 {
iconlist: Vec<Icon>,
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
// 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)?;
// Search for and filter
let favicons = soup
.tag("link")
.attr("rel", ICON_REL_REGEX.clone()) // Only use icon rels
.attr_name("href") // Make sure there is a href
.find_all();
use html5ever::tendril::TendrilSink;
let dom = html5ever::parse_document(markup5ever_rcdom::RcDom::default(), Default::default())
.from_utf8()
.read_from(&mut limited_reader)?;
// Loop through all the found icons and determine it's priority
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))
}
get_favicons_node(&dom.document, &mut iconlist, &url);
} else {
// Add the default favicon.ico to the list with just the given domain
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");
/// 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
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("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 height: u16 = 0;

View File

@@ -11,6 +11,7 @@ use serde_json::Value;
pub use crate::api::{
admin::routes as admin_routes,
core::routes as core_routes,
core::start_send_deletion_scheduler,
icons::routes as icons_routes,
identity::routes as identity_routes,
notifications::routes as notifications_routes,

View File

@@ -5,7 +5,7 @@ use rocket_contrib::json::Json;
use serde_json::Value as JsonValue;
use crate::{
api::{EmptyResult, JsonResult},
api::EmptyResult,
auth::Headers,
db::DbConn,
Error, CONFIG,
@@ -31,7 +31,7 @@ fn websockets_err() -> EmptyResult {
}
#[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
fn negotiate(_headers: Headers, _conn: DbConn) -> Json<JsonValue> {
use crate::crypto;
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
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
Ok(Json(json!({
Json(json!({
"connectionId": conn_id,
"availableTransports": available_transports
})))
}))
}
//
@@ -120,7 +120,7 @@ fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
}
// Server WebSocket handler
pub struct WSHandler {
pub struct WsHandler {
out: Sender,
user_uuid: Option<String>,
users: WebSocketUsers,
@@ -140,7 +140,7 @@ const PING: Token = Token(1);
const ACCESS_TOKEN_KEY: &str = "access_token=";
impl WSHandler {
impl WsHandler {
fn err(&self, msg: &'static str) -> ws::Result<()> {
self.out.close(ws::CloseCode::Invalid)?;
@@ -166,8 +166,8 @@ impl WSHandler {
if let Some(params) = path.split('?').nth(1) {
let params_iter = params.split('&').take(1);
for val in params_iter {
if val.starts_with(ACCESS_TOKEN_KEY) {
return Some(val[ACCESS_TOKEN_KEY.len()..].into());
if let Some(stripped) = val.strip_prefix(ACCESS_TOKEN_KEY) {
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<()> {
// Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
//
@@ -240,13 +240,13 @@ impl Handler for WSHandler {
}
}
struct WSFactory {
struct WsFactory {
pub users: WebSocketUsers,
}
impl WSFactory {
impl WsFactory {
pub fn init() -> Self {
WSFactory {
WsFactory {
users: WebSocketUsers {
map: Arc::new(CHashMap::new()),
},
@@ -254,11 +254,11 @@ impl WSFactory {
}
}
impl Factory for WSFactory {
type Handler = WSHandler;
impl Factory for WsFactory {
type Handler = WsHandler;
fn connection_made(&mut self, out: Sender) -> Self::Handler {
WSHandler {
WsHandler {
out,
user_uuid: None,
users: self.users.clone(),
@@ -394,6 +394,10 @@ pub enum UpdateType {
LogOut = 11,
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
None = 100,
}
@@ -401,15 +405,17 @@ use rocket::State;
pub type Notify<'a> = State<'a, WebSocketUsers>;
pub fn start_notification_server() -> WebSocketUsers {
let factory = WSFactory::init();
let factory = WsFactory::init();
let users = factory.users.clone();
if CONFIG.websocket_enabled() {
thread::spawn(move || {
let mut settings = ws::Settings::default();
settings.max_connections = 500;
settings.queue_size = 2;
settings.panic_on_internal = false;
let settings = ws::Settings {
max_connections: 500,
queue_size: 2,
panic_on_internal: false,
..Default::default()
};
ws::Builder::new()
.with_settings(settings)

View File

@@ -10,7 +10,7 @@ pub fn routes() -> Vec<Route> {
// If addding more routes here, consider also adding them to
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
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 {
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()
}
#[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")]
fn alive() -> Json<String> {
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-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"))),
"datatables.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
"datatables.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),

View File

@@ -58,28 +58,28 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
.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())
}
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())
}
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())
}
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())
}
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())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginJWTClaims {
pub struct LoginJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
@@ -110,7 +110,7 @@ pub struct LoginJWTClaims {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteJWTClaims {
pub struct InviteJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
@@ -132,9 +132,9 @@ pub fn generate_invite_claims(
org_id: Option<String>,
user_org_id: Option<String>,
invited_by_email: Option<String>,
) -> InviteJWTClaims {
) -> InviteJwtClaims {
let time_now = Utc::now().naive_utc();
InviteJWTClaims {
InviteJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::days(5)).timestamp(),
iss: JWT_INVITE_ISSUER.to_string(),
@@ -147,7 +147,7 @@ pub fn generate_invite_claims(
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteJWTClaims {
pub struct DeleteJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
@@ -158,9 +158,9 @@ pub struct DeleteJWTClaims {
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();
DeleteJWTClaims {
DeleteJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::days(5)).timestamp(),
iss: JWT_DELETE_ISSUER.to_string(),
@@ -169,7 +169,7 @@ pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyEmailJWTClaims {
pub struct VerifyEmailJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
@@ -180,9 +180,9 @@ pub struct VerifyEmailJWTClaims {
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();
DeleteJWTClaims {
DeleteJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::days(5)).timestamp(),
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
@@ -191,7 +191,7 @@ pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminJWTClaims {
pub struct AdminJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
@@ -202,9 +202,9 @@ pub struct AdminJWTClaims {
pub sub: String,
}
pub fn generate_admin_claims() -> AdminJWTClaims {
pub fn generate_admin_claims() -> AdminJwtClaims {
let time_now = Utc::now().naive_utc();
AdminJWTClaims {
AdminJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::minutes(20)).timestamp(),
iss: JWT_ADMIN_ISSUER.to_string(),
@@ -222,13 +222,12 @@ use crate::db::{
DbConn,
};
pub struct Headers {
pub host: String,
pub device: Device,
pub user: User,
pub struct Host {
pub host: String
}
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
impl<'a, 'r> FromRequest<'a, 'r> for Host {
type Error = &'static str;
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)
};
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
let access_token: &str = match headers.get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
@@ -429,12 +450,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
}
}
impl Into<Headers> for AdminHeaders {
fn into(self) -> Headers {
impl From<AdminHeaders> for Headers {
fn from(h: AdminHeaders) -> Headers {
Headers {
host: self.host,
device: self.device,
user: self.user,
host: h.host,
device: h.device,
user: h.user,
}
}
}
@@ -508,12 +529,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders {
}
}
impl Into<Headers> for ManagerHeaders {
fn into(self) -> Headers {
impl From<ManagerHeaders> for Headers {
fn from(h: ManagerHeaders) -> Headers {
Headers {
host: self.host,
device: self.device,
user: self.user,
host: h.host,
device: h.device,
user: h.user,
}
}
}
@@ -550,12 +571,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeadersLoose {
}
}
impl Into<Headers> for ManagerHeadersLoose {
fn into(self) -> Headers {
impl From<ManagerHeadersLoose> for Headers {
fn from(h: ManagerHeadersLoose) -> Headers {
Headers {
host: self.host,
device: self.device,
user: self.user,
host: h.host,
device: h.device,
user: h.user,
}
}
}

View File

@@ -299,6 +299,8 @@ make_config! {
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
/// Attachments folder
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: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates");
/// Session JWT key

View File

@@ -37,6 +37,7 @@ macro_rules! generate_connections {
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
#[allow(non_camel_case_types)]
#[derive(Clone)]
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
impl DbPool {
@@ -124,16 +125,34 @@ macro_rules! db_run {
$($(
#[cfg($db)]
crate::db::DbConn::$db(ref $conn) => {
paste::paste! {
paste::paste! {
#[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *};
#[allow(unused)] use [<__ $db _model>]::*;
#[allow(unused)] use crate::db::FromDb;
#[allow(unused)] use crate::db::FromDb;
}
$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;
}
// 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!
#[macro_export]
macro_rules! db_object {
@@ -153,10 +172,10 @@ macro_rules! db_object {
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
$(,)?
}
)+ ) => {
)+ ) => {
// Create the normal struct, without attributes
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
#[cfg(sqlite)]
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
#[cfg(mysql)]
@@ -177,7 +196,7 @@ macro_rules! db_object {
)+ }
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(), )+ } }
}
@@ -221,6 +240,36 @@ pub fn backup_database() -> Result<(), Error> {
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
/// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status.
@@ -262,7 +311,7 @@ mod sqlite_migrations {
let connection =
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?;
// Disable Foreign Key Checks during migration
// Scoped to a connection.
diesel::sql_query("PRAGMA foreign_keys = OFF")
.execute(&connection)
@@ -313,7 +362,7 @@ mod postgresql_migrations {
let connection =
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?;
// Disable Foreign Key Checks during migration
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
// "SET CONSTRAINTS sets the behavior of constraint checking within the
// current transaction", so this setting probably won't take effect for

View File

@@ -4,7 +4,7 @@ use super::Cipher;
use crate::CONFIG;
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "attachments"]
#[changeset_options(treat_none_as_null="true")]
#[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]

View File

@@ -14,7 +14,7 @@ use super::{
};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "ciphers"]
#[changeset_options(treat_none_as_null="true")]
#[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 '{}'.
// 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
// Set the first element of the Uris array as Uri, this is needed several (mobile) clients.

View File

@@ -3,7 +3,7 @@ use serde_json::Value;
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "collections"]
#[belongs_to(Organization, foreign_key = "org_uuid")]
#[primary_key(uuid)]
@@ -13,7 +13,7 @@ db_object! {
pub name: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Identifiable, Queryable, Insertable, Associations)]
#[table_name = "users_collections"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]
@@ -25,7 +25,7 @@ db_object! {
pub hide_passwords: bool,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers_collections"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]

View File

@@ -4,7 +4,7 @@ use super::User;
use crate::CONFIG;
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "devices"]
#[changeset_options(treat_none_as_null="true")]
#[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();
// Create the JWT claims struct, to send to the client
use crate::auth::{encode_jwt, LoginJWTClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
let claims = LoginJWTClaims {
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
let claims = LoginJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
iss: JWT_LOGIN_ISSUER.to_string(),
@@ -117,7 +117,7 @@ impl Device {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
db_run! { conn:
sqlite, mysql {
crate::util::retry(
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),

View File

@@ -1,7 +1,7 @@
use super::{Cipher, User};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Identifiable, Queryable, Insertable, Associations)]
#[table_name = "favorites"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]

View File

@@ -4,7 +4,7 @@ use serde_json::Value;
use super::{Cipher, User};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "folders"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
@@ -16,7 +16,7 @@ db_object! {
pub name: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Identifiable, Queryable, Insertable, Associations)]
#[table_name = "folders_ciphers"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[belongs_to(Folder, foreign_key = "folder_uuid")]

View File

@@ -8,6 +8,7 @@ mod org_policy;
mod organization;
mod two_factor;
mod user;
mod send;
pub use self::attachment::Attachment;
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::two_factor::{TwoFactor, TwoFactorType};
pub use self::user::{Invitation, User, UserStampException};
pub use self::send::{Send, SendType};

View File

@@ -4,10 +4,10 @@ use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use super::{Organization, UserOrgStatus};
use super::{Organization, UserOrganization, UserOrgStatus, UserOrgType};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "org_policies"]
#[belongs_to(Organization, foreign_key = "org_uuid")]
#[primary_key(uuid)]
@@ -20,7 +20,7 @@ db_object! {
}
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
#[derive(num_derive::FromPrimitive)]
pub enum OrgPolicyType {
TwoFactorAuthentication = 0,
@@ -29,6 +29,7 @@ pub enum OrgPolicyType {
// SingleOrg = 3, // Not currently supported.
// RequireSso = 4, // Not currently supported.
PersonalOwnership = 5,
DisableSend = 6,
}
/// 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 {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))

View File

@@ -5,7 +5,7 @@ use num_traits::FromPrimitive;
use super::{CollectionUser, User, OrgPolicy};
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "organizations"]
#[primary_key(uuid)]
pub struct Organization {
@@ -14,7 +14,7 @@ db_object! {
pub billing_email: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "users_organizations"]
#[primary_key(uuid)]
pub struct UserOrganization {
@@ -90,17 +90,11 @@ impl PartialOrd<i32> for UserOrgType {
}
fn gt(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) => false,
_ => true,
}
matches!(self.partial_cmp(other), Some(Ordering::Greater))
}
fn ge(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) => false,
_ => true,
}
matches!(self.partial_cmp(other), Some(Ordering::Greater) | Some(Ordering::Equal))
}
}
@@ -119,17 +113,11 @@ impl PartialOrd<UserOrgType> for i32 {
}
fn lt(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | None => true,
_ => false,
}
matches!(self.partial_cmp(other), Some(Ordering::Less) | None)
}
fn le(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
_ => false,
}
matches!(self.partial_cmp(other), Some(Ordering::Less) | Some(Ordering::Equal) | None)
}
}

274
src/db/models/send.rs Normal file
View 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()
}}
}
}

View File

@@ -7,7 +7,7 @@ use crate::error::MapResult;
use super::User;
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "twofactor"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]

View File

@@ -5,7 +5,7 @@ use crate::crypto;
use crate::CONFIG;
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "users"]
#[changeset_options(treat_none_as_null="true")]
#[primary_key(uuid)]
@@ -47,7 +47,7 @@ db_object! {
}
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[derive(Identifiable, Queryable, Insertable)]
#[table_name = "invitations"]
#[primary_key(email)]
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::api::EmptyResult;
@@ -263,6 +263,7 @@ impl User {
}
}
Send::delete_all_by_user(&self.uuid, conn)?;
UserOrganization::delete_all_by_user(&self.uuid, conn)?;
Cipher::delete_all_by_user(&self.uuid, conn)?;
Favorite::delete_all_by_user(&self.uuid, conn)?;

View File

@@ -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! {
twofactor (uuid) {
uuid -> Text,
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(org_policies -> organizations (org_uuid));
joinable!(sends -> organizations (organization_uuid));
joinable!(sends -> users (user_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
invitations,
org_policies,
organizations,
sends,
twofactor,
users,
users_collections,

View File

@@ -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! {
twofactor (uuid) {
uuid -> Text,
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(org_policies -> organizations (org_uuid));
joinable!(sends -> organizations (organization_uuid));
joinable!(sends -> users (user_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
invitations,
org_policies,
organizations,
sends,
twofactor,
users,
users_collections,

View File

@@ -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! {
twofactor (uuid) {
uuid -> Text,
@@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(org_policies -> organizations (org_uuid));
joinable!(sends -> organizations (organization_uuid));
joinable!(sends -> users (user_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
@@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!(
invitations,
org_policies,
organizations,
sends,
twofactor,
users,
users_collections,

View File

@@ -38,11 +38,11 @@ use diesel::ConnectionError as DieselConErr;
use diesel_migrations::RunMigrationsError as DieselMigErr;
use diesel::r2d2::PoolError as R2d2Err;
use handlebars::RenderError as HbErr;
use jsonwebtoken::errors::Error as JWTErr;
use jsonwebtoken::errors::Error as JwtErr;
use regex::Error as RegexErr;
use reqwest::Error as ReqErr;
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 u2f::u2ferror::U2fError as U2fErr;
@@ -72,10 +72,10 @@ make_error! {
R2d2Error(R2d2Err): _has_source, _api_error,
U2fError(U2fErr): _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,
//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,
ReqError(ReqErr): _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> {
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_rules! err_discard {
($msg:expr, $data:expr) => {{

View File

@@ -30,10 +30,10 @@ fn mailer() -> SmtpTransport {
let smtp_client = if CONFIG.smtp_ssl() {
let mut tls_parameters = TlsParameters::builder(host);
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() {
tls_parameters.dangerous_accept_invalid_certs(true);
tls_parameters = tls_parameters.dangerous_accept_invalid_certs(true);
}
let tls_parameters = tls_parameters.build().unwrap();
@@ -58,7 +58,7 @@ fn mailer() -> SmtpTransport {
let smtp_client = match CONFIG.smtp_auth_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![];
for wanted_mechanism in mechanism.split(',') {
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() }))?;
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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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(
@@ -200,15 +200,15 @@ pub fn send_invite(
"email/send_org_invite",
json!({
"url": CONFIG.domain(),
"org_id": org_id.unwrap_or_else(|| "_".to_string()),
"org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()),
"org_id": org_id.as_deref().unwrap_or("_"),
"org_user_id": org_user_id.as_deref().unwrap_or("_"),
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"org_name": org_name,
"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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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();
if address_split.len() != 2 {
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.
.header(header::ContentTransferEncoding::Base64)
.header(header::ContentType("text/html; charset=utf-8".parse()?))
.body(String::from(body_html));
.body(body_html);
let text = SinglePart::builder()
// We force Base64 encoding because in the past we had issues with different encodings.
.header(header::ContentTransferEncoding::Base64)
.header(header::ContentType("text/plain; charset=utf-8".parse()?))
.body(String::from(body_text));
.body(body_text);
let smtp_from = &CONFIG.smtp_from();
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) {
Ok(_) => Ok(()),
// Match some common errors and make them more user friendly
Err(e) => match e {
lettre::transport::smtp::Error::Client(x) => {
err!(format!("SMTP Client error: {}", x));
},
lettre::transport::smtp::Error::Transient(x) => {
err!(format!("SMTP 4xx error: {:?}", x.message));
},
lettre::transport::smtp::Error::Permanent(x) => {
err!(format!("SMTP 5xx error: {:?}", x.message));
},
lettre::transport::smtp::Error::Io(x) => {
err!(format!("SMTP IO error: {}", x));
},
// Fallback for all other errors
_ => Err(e.into())
Err(e) => {
if e.is_client() {
err!(format!("SMTP Client error: {}", e));
} else if e.is_transient() {
err!(format!("SMTP 4xx error: {:?}", e));
} else if e.is_permanent() {
err!(format!("SMTP 5xx error: {:?}", e));
} else if e.is_timeout() {
err!(format!("SMTP timeout error: {:?}", e));
} else {
Err(e.into())
}
}
}
}

View File

@@ -25,8 +25,6 @@ use std::{
thread,
};
use structopt::StructOpt;
#[macro_use]
mod error;
mod api;
@@ -40,14 +38,7 @@ mod util;
pub use config::CONFIG;
pub use error::{Error, MapResult};
#[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,
}
pub use util::is_running_in_docker;
fn main() {
parse_args();
@@ -57,11 +48,9 @@ fn main() {
let level = LF::from_str(&CONFIG.log_level()).expect("Valid log level");
init_logging(level).ok();
let extra_debug = match level {
LF::Trace | LF::Debug => true,
_ => false,
};
let extra_debug = matches!(level, LF::Trace | LF::Debug);
check_data_folder();
check_rsa_keys();
check_web_vault();
@@ -70,14 +59,27 @@ fn main() {
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() {
let opt = Opt::from_args();
if opt.version {
if let Some(version) = option_env!("BWRS_VERSION") {
println!("bitwarden_rs {}", version);
} else {
println!("bitwarden_rs (Version info from Git not present)");
}
const NO_VERSION: &str = "(Version info from Git not present)";
let mut pargs = pico_args::Arguments::from_env();
if pargs.contains(["-h", "--help"]) {
println!("bitwarden_rs {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
print!("{}", HELP);
exit(0);
} else if pargs.contains(["-v", "--version"]) {
println!("bitwarden_rs {}", option_env!("BWRS_VERSION").unwrap_or(NO_VERSION));
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() {
// Try to create the icon cache folder, and generate an error if it could not.
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory");
create_dir(&CONFIG.icon_cache_folder(), "icon cache");
}
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() {
@@ -289,6 +310,8 @@ fn launch_rocket(extra_debug: bool) {
}
};
api::start_send_deletion_scheduler(pool.clone());
let basepath = &CONFIG.domain_path();
// If adding more paths here, consider also adding them to
@@ -303,7 +326,7 @@ fn launch_rocket(extra_debug: bool) {
.manage(pool)
.manage(api::start_notification_server())
.attach(util::AppHeaders())
.attach(util::CORS())
.attach(util::Cors())
.attach(util::BetterLogging(extra_debug))
.launch();

View File

@@ -894,5 +894,13 @@
"sony.com"
],
"Excluded": false
},
{
"Type": 85,
"Domains": [
"protonmail.com",
"protonvpn.com"
],
"Excluded": false
}
]

View File

@@ -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)

View File

@@ -20,7 +20,6 @@
width: auto;
}
</style>
<script src="{{urlpath}}/bwrs_static/md5.js"></script>
<script src="{{urlpath}}/bwrs_static/identicon.js"></script>
<script>
function reload() { window.location.reload(); }
@@ -28,8 +27,17 @@
text && alert(text);
reload_page && reload();
}
function identicon(email) {
const data = new Identicon(md5(email), { size: 48, format: 'svg' });
async function sha256(message) {
// 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();
}
function toggleVis(input_id) {
@@ -125,4 +133,4 @@
<!-- This script needs to be at the bottom, else it will fail! -->
<script src="{{urlpath}}/bwrs_static/bootstrap-native.js"></script>
</body>
</html>
</html>

View File

@@ -2,7 +2,7 @@
<div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow">
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>
<h3>Version</h3>
<h3>Versions</h3>
<div class="row">
<div class="col-md">
<dl class="row">
@@ -35,6 +35,10 @@
<span id="web-latest">{{diagnostics.latest_web_build}}</span>
</dd>
{{/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>
</div>
</div>
@@ -46,35 +50,65 @@
<dt class="col-sm-5">Running within Docker</dt>
<dd class="col-sm-7">
{{#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}}
{{#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}}
</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">
{{#if diagnostics.uses_proxy}}
<span id="running-docker" class="d-block"><b>Yes</b></span>
{{#if diagnostics.ip_header_exists}}
<span class="d-block" title="IP Header found."><b>Yes</b></span>
{{/if}}
{{#unless diagnostics.uses_proxy}}
<span id="running-docker" class="d-block"><b>No</b></span>
{{#unless diagnostics.ip_header_exists}}
<span class="d-block" title="No IP Header found."><b>No</b></span>
{{/unless}}
</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
{{#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}}
{{#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}}
</dt>
<dd class="col-sm-7">
{{#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}}
{{#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}}
</dd>
<dt class="col-sm-5">DNS (github.com)
@@ -263,16 +297,18 @@
supportString += "* Bitwarden_rs version: v{{ version }}\n";
supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\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 += "* Uses a proxy: {{ diagnostics.uses_proxy }}\n";
supportString += "* Internet access via a proxy: {{ diagnostics.uses_proxy }}\n";
supportString += "* DNS Check: " + dnsCheck + "\n";
supportString += "* Time Check: " + timeCheck + "\n";
supportString += "* Domain Configuration Check: " + domainCheck + "\n";
supportString += "* HTTPS Check: " + httpsCheck + "\n";
supportString += "* Database type: {{ diagnostics.db_type }}\n";
{{#case diagnostics.db_type "MySQL" "PostgreSQL"}}
supportString += "* Database version: [PLEASE PROVIDE DATABASE VERSION]\n";
{{/case}}
supportString += "* Database version: {{ diagnostics.db_version }}\n";
supportString += "* Clients used: \n";
supportString += "* Reverse proxy and version: \n";
supportString += "* Other relevant information: \n";

View File

@@ -73,9 +73,11 @@
return false;
}
document.querySelectorAll("img.identicon").forEach(function (e, i) {
e.src = identicon(e.dataset.src);
});
(async () => {
for (let e of document.querySelectorAll("img.identicon")) {
e.src = await identicon(e.dataset.src);
}
})();
document.addEventListener("DOMContentLoaded", function(event) {
$('#orgs-table').DataTable({
@@ -87,4 +89,4 @@
]
});
});
</script>
</script>

View File

@@ -206,9 +206,11 @@
"3": { "name": "Manager", "color": "green" },
};
document.querySelectorAll("img.identicon").forEach(function (e, i) {
e.src = identicon(e.dataset.src);
});
(async () => {
for (let e of document.querySelectorAll("img.identicon")) {
e.src = await identicon(e.dataset.src);
}
})();
document.querySelectorAll("[data-orgtype]").forEach(function (e, i) {
let orgtype = OrgTypes[e.dataset.orgtype];
@@ -288,4 +290,4 @@
"Error updating organization type of the user", data);
return false;
}
</script>
</script>

View File

@@ -23,7 +23,7 @@ impl Fairing for AppHeaders {
}
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("X-Frame-Options", "SAMEORIGIN");
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 {
match headers.get_one(name) {
Some(h) => h.to_string(),
@@ -48,18 +48,24 @@ impl CORS {
}
}
fn valid_url(url: String) -> String {
match url.as_ref() {
"file://" => "*".to_string(),
_ => url,
// Check a request's `Origin` header against the list of allowed origins.
// If a match exists, return it. Otherwise, return None.
fn get_allowed_origin(headers: &HeaderMap) -> Option<String> {
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 {
Info {
name: "CORS",
name: "Cors",
kind: Kind::Response,
}
}
@@ -67,14 +73,14 @@ impl Fairing for CORS {
fn on_response(&self, request: &Request, response: &mut Response) {
let req_headers = request.headers();
// We need to explicitly get the Origin header for Access-Control-Allow-Origin
let req_allow_origin = CORS::valid_url(CORS::get_header(req_headers, "Origin"));
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
if let Some(origin) = Cors::get_allowed_origin(req_headers) {
response.set_header(Header::new("Access-Control-Allow-Origin", origin));
}
// Preflight request
if request.method() == Method::Options {
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_headers = Cors::get_header(req_headers, "Access-Control-Request-Headers");
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-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> {
pub const fn long(r: R) -> Cached<R> {
pub fn long(r: R) -> Cached<R> {
// 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
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)
}
//
// 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
//