Compare commits

...

104 Commits
1.13.0 ... 1.14

Author SHA1 Message Date
Daniel García
70f3ab8ec3 Migrate lazy_static to once_cell, less macro magic and slightly faster 2020-03-09 22:04:03 +01:00
Daniel García
b6612e90ca Update dependencies 2020-03-09 22:00:59 +01:00
Daniel García
161cccca30 Merge pull request #892 from BlackDex/smtp-test-button
Relocated SMTP test input+button.
2020-03-05 00:05:52 +01:00
BlackDex
84dc2eda1f Changed javascript default argument construction 2020-03-04 15:08:14 +01:00
BlackDex
390d10d656 Relocated SMTP test input+button.
- Moved smtp test option to within the "SMTP Email" Settings block.
- Added optional option to prevent full page reload.
- SMTP Test and Backup do not reload the admin interface any more.
2020-03-04 13:25:38 +01:00
Daniel García
1f775f4414 Merge pull request #888 from zethra/add_cli_args
Added command line flags for help and version
2020-03-03 00:12:15 +01:00
zethra
cc404b4edc Added command line flags for help and version
Signed-off-by: zethra <benaagoldberg@gmail.com>
2020-03-02 15:51:57 -05:00
Daniel García
536672ac1b Delete ISSUE_TEMPLATE.md 2020-03-02 19:58:53 +01:00
Daniel García
e41e7c07db Update issue templates 2020-03-02 19:58:36 +01:00
Daniel García
f1d3b03c60 Update README.md 2020-03-02 19:37:49 +01:00
Daniel García
2ebff958a4 Merge pull request #879 from BlackDex/smtp-test-button
Added SMTP test button in the admin gui
2020-03-01 15:00:20 +01:00
Daniel García
edfdda86ae Use web vault built by docker autobuild, using the hash to reference the image for extra security 2020-03-01 02:40:18 +01:00
BlackDex
97fb7b5b96 Added urlpath to smtpTest function 2020-02-26 16:58:57 +01:00
BlackDex
f6de144cbb Merge branch 'smtp-test-button' of github.com:BlackDex/bitwarden_rs into smtp-test-button 2020-02-26 16:56:03 +01:00
BlackDex
5a974c7b94 Added SMTP test button in the admin gui
- Added a test button for checking the e-mail settings.
- Fixed a bug with the _post JavaScript function:
  A function was overwriten with a variable and errors were not handled
correctly like a 500 for example.
2020-02-26 16:49:56 +01:00
BlackDex
5f61607419 Added SMTP test button in the admin gui
- Added a test button for checking the e-mail settings.
- Fixed a bug with the _post JavaScript function:
  A function was overwriten with a variable and errors were not handled
correctly like a 500 for example.
2020-02-26 11:02:22 +01:00
Daniel García
cd8907542a Make sure the provided domain contains the protocol and show a useful error when it doesn't 2020-02-23 14:55:27 +01:00
Daniel García
8a5450e830 Merge pull request #868 from jjlin/alt-base
Add backend support for alternate base dir (subdir/subpath) hosting
2020-02-22 22:06:07 +01:00
Daniel García
ad9f2b2d8e Removed test urlpath 2020-02-22 19:01:58 +01:00
Daniel García
2f4a9865e1 Use absolute paths in the admin page 2020-02-22 17:49:33 +01:00
Daniel García
0a3008e753 Update web vault used in docker 2020-02-22 16:00:43 +01:00
Jeremy Lin
29a0795219 Add backend support for alternate base dir (subdir/subpath) hosting
To use this, include a path in the `DOMAIN` URL, e.g.:

* `DOMAIN=https://example.com/custom-path`
* `DOMAIN=https://example.com/multiple/levels/are/ok`
2020-02-18 21:27:00 -08:00
Daniel García
63459c5f72 Updated FUNDING as mentioned in #859 2020-02-18 21:48:11 +01:00
Daniel García
916e96b143 Update web vault to fix copy issues 2020-02-18 20:08:21 +01:00
Daniel García
325039c316 Attachment size limits, per-user and per-organization 2020-02-17 22:56:26 +01:00
Daniel García
c5b97f4146 Merge pull request #864 from mprasil/admin-invitation
Do not disable invitations via admin API
2020-02-16 22:12:00 +01:00
Miro Prasil
03233429f4 Remove check from Invitation:take()
I've checked the spots when `Invitation::new()` and `Invitation::take()`
are used and it seems like all spots are already correctly gated. So to
enable invitations via admin API even when invitations are otherwise
disabled, this check can be removed.
2020-02-16 20:28:50 +00:00
Miroslav Prasil
0a72c4b6db Do not disable invitations via admin API
This was brought up today:

https://github.com/dani-garcia/bitwarden_rs/issues/752#issuecomment-586715073

I don't think it makes much sense in checking whether admin has the
right to send invitation as admin can change the setting anyway.

Removing the condition allows users to forbid regular users from
inviting new users to server while still preserving the option to do so
via the admin API.
2020-02-16 15:01:07 +00:00
Daniel García
8867626de8 Add option to change invitation org name, fixes #825
Add option to allow additional iframe ancestors, fixes #843
Sort the rocket routes before printing them
2020-02-04 22:14:50 +01:00
Daniel García
f5916ec396 Fix backwards indices 2020-01-30 22:33:50 +01:00
Daniel García
ebb36235a7 Cache icons in the clients 2020-01-30 22:30:57 +01:00
Daniel García
def174a517 Convert email domains to punycode 2020-01-30 22:11:53 +01:00
Daniel García
2798f623d4 Updated rust toolchain version 2020-01-30 22:11:44 +01:00
Daniel García
480ba933fa Don't error if admin token is empty but disabled 2020-01-30 22:10:50 +01:00
Daniel García
3d1ee9ef62 Use rust-toolchain file to determine version in workflows, disabled mac builds for now 2020-01-29 19:26:06 +01:00
Daniel García
5352321fe1 Merge pull request #831 from mprasil/whitelist-fix
SIGNUPS_ALLOWED with no whitelist [fixes #830]
2020-01-29 13:28:07 +01:00
Miro Prasil
c4101162d6 SIGNUPS_ALLOWED with no whitelist [fixes #830]
This reverts back to `SIGNUPS_ALLOWED` when there is no domain whitelist
set. The functionality was broken in 64d6f72.
2020-01-29 11:32:42 +00:00
Daniel García
632d55265b Merge pull request #824 from tomuta/fix_change_email
Fix change email when no whitelist is configured
2020-01-28 20:52:16 +01:00
tomuta
e277f7d1c1 Fix change email when no whitelist is configured
Fixes issue #792
2020-01-26 13:34:56 -07:00
Daniel García
ff7b4a3d38 Update handlebars to 3.0 which included performance improvements.
Updated lettre to newer git revision, which should give better error messages now.
2020-01-26 15:29:14 +01:00
Daniel García
d212dfe735 Accept y/n, True/False, 1/0 as booleans in environment vars 2020-01-20 22:28:54 +01:00
Daniel García
84ed185579 Update u2f to 0.2, which requires OpenSSL but also might solve the problems we've had with certificates.
The rust image doesn't need installing curl or tar, so removed. Also collapsed ENV lines.
2020-01-19 21:34:13 +01:00
Daniel García
c0ba3406ef Merge pull request #812 from swedishborgie/postgresql
Fixes #635 - Unique constraint violation when using U2F tokens on PostgreSQL
2020-01-16 16:21:57 +01:00
Michael Powers
e196ba6e86 Switch error handling to ? operator instead of explicit handling. 2020-01-16 08:14:25 -05:00
Michael Powers
76743aee48 Fixes #635 - Unique constraint violation when using U2F tokens on PostgreSQL
Because of differences in how .on_conflict() works compared to .replace_into() the PostgreSQL backend wasn't correctly ensuring the unique constraint on user_uuid and atype wasn't getting violated.

This change simply issues a DELETE on the unique constraint prior to the insert to ensure uniqueness. PostgreSQL does not support multiple constraints in ON CONFLICT clauses.
2020-01-13 21:53:57 -05:00
Daniel García
9ebca99290 Update dependencies 2020-01-10 18:37:16 +01:00
Daniel García
a734ad2d36 Add contributor 2020-01-10 18:36:36 +01:00
Daniel García
baf7d1be4e Delete old workflow file and disable building binaries on pull requests, as we already have CI for that 2020-01-05 22:46:34 +01:00
Daniel García
31bcd1bf7c Merge pull request #784 from ypid/docker/use-debian-base
Use Debian base image for all steps of the build process
2020-01-05 22:42:43 +01:00
Daniel García
a3b30ed65a Add missing target armv7 and cross compile envs 2020-01-05 22:41:58 +01:00
Daniel García
59e50b03bd Merge pull request #804 from publicarray/master
Improve Github Actions Workflow
2020-01-05 18:00:15 +01:00
Sebastian Schmidt
0a88f020e1 Disable Windows workflow 2020-01-05 20:45:03 +11:00
Daniel García
c058a1d63c Make sure handlebars is not updated, as the next patch version has breaking changes 2020-01-05 00:12:35 +01:00
Daniel García
96a189deb9 Merge pull request #803 from aeolyus/master
Minor typo conect -> connect
2020-01-05 00:12:15 +01:00
Daniel García
8c229920ad Protect websocket server against panics 2020-01-04 23:52:38 +01:00
Richard Huang
d592323e39 minor typo conect -> connect 2020-01-04 14:37:29 -08:00
Robin Schneider
402c857d17 Add hint to Dockerfile's that they are generated 2020-01-03 22:07:56 +01:00
Robin Schneider
def858854b Readd missing cargo build for armv7. Thanks to @dani-garcia! 2020-01-03 22:00:45 +01:00
Robin Schneider
f6761ac30e Remove debugging echo statement from Dockerfiles 2020-01-01 15:17:33 +01:00
Robin Schneider
f8e49ea3f4 Use apt-get instead of apt in Dockerfiles, also --no-install-recommends
apt is intended for humans, not scripts.

--no-install-recommends improves build time by avoiding to install
unneeded packages.
2019-12-31 16:46:08 +01:00
Robin Schneider
f6a4a2127b Remove duplicate empty lines in generated Dockerfiles
Checked with:

```Shell
find . -type f -print0 | xargs -0 pcregrep -M '\n\n\n'
```
2019-12-31 16:33:00 +01:00
Robin Schneider
446fc3f1f8 Set build time options for dpkg and reproducible builds
Ref: https://github.com/moby/moby/issues/4032
Ref: https://sweetcode.io/using-docker-reproducible-build-environments/
Ref: https://github.com/hashbang/aosp-build/blob/master/config/container/Dockerfile
2019-12-31 16:33:00 +01:00
Robin Schneider
146525db91 Improve Jinja2 template logic a bit 2019-12-31 16:33:00 +01:00
Robin Schneider
1698b43f9b Readd missing cargo setup for armv7. Thanks to @dani-garcia! 2019-12-31 16:33:00 +01:00
Robin Schneider
078b21db85 Fix armv6 build, thanks to @dani-garcia for the review! 2019-12-31 16:33:00 +01:00
Robin Schneider
43adcde094 Move rustup target before cargo build. Thanks to @dani-garcia!
Note from @dani-garcia:

> I don't think this is doing anything right now because the target is probably
> installed already.
2019-12-31 16:32:59 +01:00
Daniel García
7a0bb18dcf Make cargo new independent of workdir to be exact
The muslrust images seem to have a workdir of /volume as opposed to / in the
others so doing cargo new like this would create the folder in /volume/app.
2019-12-31 16:32:59 +01:00
Robin Schneider
47a5a4e1fc Fix package name for Ubuntu 16.04 based image. Thanks @dani-garcia! 2019-12-31 16:32:59 +01:00
Robin Schneider
0f0e5876ae Move dpkg --add-architecture before the first apt call
Thanks to @dani-garcia for the review!
2019-12-31 16:32:59 +01:00
Robin Schneider
43aa75dc89 Fix cross platform build support, thanks to @dani-garcia for the review 2019-12-31 16:32:59 +01:00
Daniel García
95dd1cd7ad Use rmp upstream version 2019-12-31 02:00:16 +01:00
Daniel García
36ae946655 Avoid some to_string in the request logging and include message to disable web vault when not found. 2019-12-29 15:34:22 +01:00
Sebastian Schmidt
24edc94f9d try setting VCPKG_ROOT 2019-12-29 19:06:54 +11:00
Sebastian Schmidt
4deae76347 Update build workflow 2019-12-29 17:20:29 +11:00
Robin Schneider
8280d200ea Generate Dockerfiles from one source for maintainability. Closes #785. 2019-12-28 22:52:20 +01:00
Daniel García
8ee0c57224 Disable Windows build for now to avoid failing CI 2019-12-28 15:28:22 +01:00
Daniel García
cb6f392774 When receiving a comma separated list as IP, pick the first 2019-12-28 15:09:07 +01:00
Robin Schneider
f250c54813 WIP: Use Debian base image for all steps of the build process
No need to use two different base images. Debian buster is pulled later
anyway so we can just use it for the vault stage as well.

My reason for this change is partly to avoid redundancy and partly to
make it easier to build everything yourself. When all the build
environment is based on Debian than you just have to figure out how to
build a Debian Docker base image (ref:
https://github.com/ypid/docker-makefile).
2019-12-28 14:43:08 +01:00
Daniel García
5c6081c4e2 Merge pull request #779 from publicarray/master
Add Github build Action
2019-12-27 22:26:01 +01:00
Daniel García
88c56de97b Config option for client IP header 2019-12-27 18:42:39 +01:00
Daniel García
e274af6e3d Print current server time when failing TOTP, and use chrono as the rest of the server 2019-12-27 18:42:14 +01:00
Daniel García
a0ece3754b Formatting 2019-12-27 18:37:14 +01:00
Sebastian Schmidt
0bcc2ae7ab Update rust-win.yml 2019-12-25 12:50:57 +11:00
Sebastian Schmidt
bdb90460c4 Update rust-win.yml 2019-12-25 11:59:07 +11:00
Sebastian Schmidt
824137a02c update dependencies to build workflows 2019-12-25 11:16:35 +11:00
Sebastian Schmidt
2edc699eac fix 2019-12-25 10:25:35 +11:00
Sebastian Schmidt
8e79366076 fix action 2019-12-25 10:23:02 +11:00
Sebastian Schmidt
c1e39b182f update build actions 2019-12-25 10:20:00 +11:00
Sebastian Schmidt
13eb276085 Create Github build Actions 2019-12-24 08:13:08 +11:00
Daniel García
4cec502f7b Update docker images to alpine 3.11 and rust 1.40 2019-12-22 21:42:13 +01:00
Daniel García
2545469713 Fix crash when page URL points to huge file 2019-12-19 00:37:16 +01:00
Daniel García
f09996a21d Updated dependencies 2019-12-15 15:43:56 +01:00
Daniel García
5cabf4d040 Fix IP not shown when failed login (Fixes #761) 2019-12-07 14:38:32 +01:00
Daniel García
a03db6d224 Also hide options requests, unless using debug or trace 2019-12-06 22:55:29 +01:00
Daniel García
8d1b72b951 Collapsed log messages from 3 lines per request to 2 and hidden the ones valued as less informative.
Use LOG_LEVEL debug or trace to recover them.

Removed LOG_MOUNTS and bundled it with LOG_LEVEL debug and trace.

Removed duplicate error messages

Made websocket not proxied message more prominent, but only print it once.
2019-12-06 22:46:12 +01:00
Daniel García
912e1f93b7 Fix some lints 2019-12-06 22:12:41 +01:00
Daniel García
a5aa4d9b54 Updated dependencies 2019-12-06 22:07:25 +01:00
Daniel García
e777be3dde Merge pull request #755 from mqus/patch-2
Create an issue template
2019-12-03 00:31:05 +01:00
Markus Richter
b5441f6b77 Include suggestions 2019-12-02 23:01:04 +01:00
mqus
dbbd63e519 Create an issue template
I'm not sure if this is needed but I think it could be useful in lessening the workload.
2019-12-02 16:06:18 +01:00
Daniel García
adc443ea80 Add endpoint to delete specific U2F key 2019-12-01 21:41:46 +01:00
Daniel García
0d32179d07 Logout button in admin page 2019-12-01 21:15:14 +01:00
Daniel García
b45b02b37e Change CI to run tests 2019-11-30 23:32:31 +01:00
Daniel García
12928b832c Fix broken tests 2019-11-30 23:30:35 +01:00
69 changed files with 3426 additions and 1985 deletions

View File

@@ -21,6 +21,10 @@
## Automatically reload the templates for every request, slow, use only for development ## Automatically reload the templates for every request, slow, use only for development
# RELOAD_TEMPLATES=false # RELOAD_TEMPLATES=false
## Client IP Header, used to identify the IP of the client, defaults to "X-Client-IP"
## Set to the string "none" (without quotes), to disable any headers and just use the remote IP
# IP_HEADER=X-Client-IP
## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever") ## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever")
# ICON_CACHE_TTL=2592000 # ICON_CACHE_TTL=2592000
## Cache time-to-live for icons which weren't available, in seconds (0 is "forever") ## Cache time-to-live for icons which weren't available, in seconds (0 is "forever")
@@ -37,14 +41,10 @@
# WEBSOCKET_ADDRESS=0.0.0.0 # WEBSOCKET_ADDRESS=0.0.0.0
# WEBSOCKET_PORT=3012 # WEBSOCKET_PORT=3012
## Enable extended logging ## Enable extended logging, which shows timestamps and targets in the logs
## This shows timestamps and allows logging to file and to syslog
### To enable logging to file, use the LOG_FILE env variable
### To enable syslog, use the USE_SYSLOG env variable
# EXTENDED_LOGGING=true # EXTENDED_LOGGING=true
## Logging to file ## Logging to file
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off' ## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log # LOG_FILE=/path/to/log
@@ -56,7 +56,8 @@
## Log level ## Log level
## Change the verbosity of the log output ## Change the verbosity of the log output
## Valid values are "trace", "debug", "info", "warn", "error" and "off" ## Valid values are "trace", "debug", "info", "warn", "error" and "off"
## This requires extended logging ## Setting it to "trace" or "debug" would also show logs for mounted
## routes and static file, websocket and alive requests
# LOG_LEVEL=Info # LOG_LEVEL=Info
## Enable WAL for the DB ## Enable WAL for the DB

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
github: dani-garcia github: dani-garcia
custom: ["https://paypal.me/DaniGG"]

42
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,42 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!--
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 unneccessary for your issue, feel free to remove them.
Remember to hide/obfuscate personal and confidential information,
such as names, global IP/DNS adresses and especially passwords, if neccessary.
-->
### Subject of the issue
<!-- Describe your issue here.-->
### Your environment
<!-- The version number, obtained from the logs or the admin page -->
* Bitwarden_rs version:
<!-- How the server was installed: Docker image / package / built from source -->
* Install method:
* Clients used: <!-- if applicable -->
* Reverse proxy and version: <!-- if applicable -->
* Version of mysql/postgresql: <!-- if applicable -->
* Other relevant information:
### 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 -->
### Actual behaviour
<!-- Tell us what happens instead -->
### Relevant logs
<!-- Share some logfiles, screenshots or output of relevant programs with us. -->

View File

@@ -0,0 +1,11 @@
---
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

@@ -0,0 +1,11 @@
---
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

@@ -0,0 +1,11 @@
---
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

148
.github/workflows/workspace.yml vendored Normal file
View File

@@ -0,0 +1,148 @@
name: Workflow
on:
push:
paths-ignore:
- "**.md"
#pull_request:
# paths-ignore:
# - "**.md"
jobs:
build:
name: Build
strategy:
fail-fast: false
matrix:
db-backend: [sqlite, mysql, postgresql]
target:
- x86_64-unknown-linux-gnu
# - x86_64-unknown-linux-musl
# - x86_64-apple-darwin
# - x86_64-pc-windows-msvc
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
ext:
# - target: x86_64-unknown-linux-musl
# os: ubuntu-latest
# ext:
# - target: x86_64-apple-darwin
# os: macOS-latest
# ext:
# - target: x86_64-pc-windows-msvc
# os: windows-latest
# ext: .exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
# - name: Cache choco cache
# uses: actions/cache@v1.0.3
# if: matrix.os == 'windows-latest'
# with:
# path: ~\AppData\Local\Temp\chocolatey
# key: ${{ runner.os }}-choco-cache-${{ matrix.db-backend }}
- name: Cache vcpkg installed
uses: actions/cache@v1.0.3
if: matrix.os == 'windows-latest'
with:
path: $VCPKG_ROOT/installed
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
env:
VCPKG_ROOT: 'C:\vcpkg'
- name: Cache vcpkg downloads
uses: actions/cache@v1.0.3
if: matrix.os == 'windows-latest'
with:
path: $VCPKG_ROOT/downloads
key: ${{ runner.os }}-vcpkg-cache-${{ matrix.db-backend }}
env:
VCPKG_ROOT: 'C:\vcpkg'
# - name: Cache homebrew
# uses: actions/cache@v1.0.3
# if: matrix.os == 'macOS-latest'
# with:
# path: ~/Library/Caches/Homebrew
# key: ${{ runner.os }}-brew-cache
# - name: Cache apt
# uses: actions/cache@v1.0.3
# if: matrix.os == 'ubuntu-latest'
# with:
# path: /var/cache/apt/archives
# key: ${{ runner.os }}-apt-cache
# Install dependencies
- name: Install dependencies macOS
run: brew update; brew install openssl sqlite libpq mysql
if: matrix.os == 'macOS-latest'
- name: Install dependencies Ubuntu
run: sudo apt-get update && sudo apt-get install --no-install-recommends openssl sqlite libpq-dev libmysql++-dev
if: matrix.os == 'ubuntu-latest'
- name: Install dependencies Windows
run: vcpkg integrate install; vcpkg install sqlite3:x64-windows openssl:x64-windows libpq:x64-windows libmysql:x64-windows
if: matrix.os == 'windows-latest'
env:
VCPKG_ROOT: 'C:\vcpkg'
# End Install dependencies
# Install rust nightly toolchain
- name: Cache cargo registry
uses: actions/cache@v1.0.3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1.0.3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1.0.3
with:
path: target
key: ${{ runner.os }}-${{matrix.db-backend}}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Install latest nightly
uses: actions-rs/toolchain@v1.0.5
with:
# Uses rust-toolchain to determine version
profile: minimal
target: ${{ matrix.target }}
# Build
- name: Build Win
if: matrix.os == 'windows-latest'
run: cargo.exe build --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
VCPKG_ROOT: 'C:\vcpkg'
- name: Build macOS / Ubuntu
if: matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest'
run: cargo build --verbose --features ${{ matrix.db-backend }} --release --target ${{ matrix.target }}
# Test
- name: Run tests
run: cargo test --features ${{ matrix.db-backend }}
# Upload & Release
- name: Upload artifact
uses: actions/upload-artifact@v1.0.0
with:
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
- name: Release
uses: Shopify/upload-to-release@1.0.0
if: startsWith(github.ref, 'refs/tags/')
with:
name: bitwarden_rs-${{ matrix.db-backend }}-${{ matrix.target }}${{ matrix.ext }}
path: target/${{ matrix.target }}/release/bitwarden_rs${{ matrix.ext }}
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -17,5 +17,5 @@ before_install:
install: true install: true
script: script:
- git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint - git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint
- cargo build --features "sqlite" - cargo test --features "sqlite"
- cargo build --features "mysql" - cargo test --features "mysql"

2648
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ build = "build.rs"
# Empty to keep compatibility, prefer to set USE_SYSLOG=true # Empty to keep compatibility, prefer to set USE_SYSLOG=true
enable_syslog = [] enable_syslog = []
mysql = ["diesel/mysql", "diesel_migrations/mysql"] mysql = ["diesel/mysql", "diesel_migrations/mysql"]
postgresql = ["diesel/postgres", "diesel_migrations/postgres", "openssl"] postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"] sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
[target."cfg(not(windows))".dependencies] [target."cfg(not(windows))".dependencies]
@@ -26,7 +26,7 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
rocket_contrib = "0.5.0-dev" rocket_contrib = "0.5.0-dev"
# HTTP client # HTTP client
reqwest = "0.9.22" reqwest = "0.9.24"
# multipart/form-data support # multipart/form-data support
multipart = { version = "0.16.1", features = ["server"], default-features = false } multipart = { version = "0.16.1", features = ["server"], default-features = false }
@@ -35,19 +35,19 @@ multipart = { version = "0.16.1", features = ["server"], default-features = fals
ws = "0.9.1" ws = "0.9.1"
# MessagePack library # MessagePack library
rmpv = "0.4.2" rmpv = "0.4.4"
# Concurrent hashmap implementation # Concurrent hashmap implementation
chashmap = "2.2.2" chashmap = "2.2.2"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = "1.0.103" serde = "1.0.104"
serde_derive = "1.0.103" serde_derive = "1.0.104"
serde_json = "1.0.42" serde_json = "1.0.48"
# Logging # Logging
log = "0.4.8" log = "0.4.8"
fern = { version = "0.5.9", features = ["syslog-4"] } fern = { version = "0.6.0", features = ["syslog-4"] }
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "1.4.3", features = [ "chrono", "r2d2"] } diesel = { version = "1.4.3", features = [ "chrono", "r2d2"] }
@@ -63,19 +63,19 @@ ring = "0.14.6"
uuid = { version = "0.8.1", features = ["v4"] } uuid = { version = "0.8.1", features = ["v4"] }
# Date and time library for Rust # Date and time library for Rust
chrono = "0.4.10" chrono = "0.4.11"
# TOTP library # TOTP library
oath = "0.10.2" oath = "0.10.2"
# Data encoding library # Data encoding library
data-encoding = "2.1.2" data-encoding = "2.2.0"
# JWT library # JWT library
jsonwebtoken = "6.0.1" jsonwebtoken = "6.0.1"
# U2F library # U2F library
u2f = "0.1.6" u2f = "0.2.0"
# Yubico Library # Yubico Library
yubico = { version = "0.7.1", features = ["online-tokio"], default-features = false } yubico = { version = "0.7.1", features = ["online-tokio"], default-features = false }
@@ -83,47 +83,47 @@ yubico = { version = "0.7.1", features = ["online-tokio"], default-features = fa
# A `dotenv` implementation for Rust # A `dotenv` implementation for Rust
dotenv = { version = "0.15.0", default-features = false } dotenv = { version = "0.15.0", default-features = false }
# Lazy static macro # Lazy initialization
lazy_static = "1.4.0" once_cell = "1.3.1"
# More derives # More derives
derive_more = "0.99.2" derive_more = "0.99.3"
# Numerical libraries # Numerical libraries
num-traits = "0.2.10" num-traits = "0.2.11"
num-derive = "0.3.0" num-derive = "0.3.0"
# Email libraries # Email libraries
lettre = "0.9.2" lettre = "0.10.0-pre"
lettre_email = "0.9.2" native-tls = "0.2.4"
native-tls = "0.2.3" quoted_printable = "0.4.2"
quoted_printable = "0.4.1"
# Template library # Template library
handlebars = "2.0.2" handlebars = { version = "3.0.1", features = ["dir_source"] }
# For favicon extraction from main website # For favicon extraction from main website
soup = "0.4.1" soup = "0.5.0"
regex = "1.3.1" regex = "1.3.4"
data-url = "0.1.0" data-url = "0.1.0"
# Required for SSL support for PostgreSQL # Used by U2F, JWT and Postgres
openssl = { version = "0.10.26", optional = true } openssl = "0.10.28"
# URL encoding library # URL encoding library
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
# Punycode conversion
idna = "0.2.0"
# CLI argument parsing
structopt = "0.3.11"
[patch.crates-io] [patch.crates-io]
# Add support for Timestamp type
rmp = { git = 'https://github.com/3Hren/msgpack-rust', rev = 'd6c6c672e470341207ed9feb69b56322b5597a11' }
# Use newest ring # Use newest ring
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' } rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' }
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' } rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'b95b6765e1cc8be7c1e7eaef8a9d9ad940b0ac13' }
# Use git version for timeout fix #706 # Use git version for timeout fix #706
lettre = { git = 'https://github.com/lettre/lettre', rev = '24d694db3be017d82b1cdc8bf9da601420b31bb0' } lettre = { git = 'https://github.com/lettre/lettre', rev = '245c600c82ee18b766e8729f005ff453a55dce34' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = '24d694db3be017d82b1cdc8bf9da601420b31bb0' }
# For favicon extraction from main website # For favicon extraction from main website
data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' } data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev = '7f1bd6ce1c2fde599a757302a843a60e714c5f72' }

View File

@@ -13,7 +13,7 @@ Image is based on [Rust implementation of Bitwarden API](https://github.com/dani
**This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.** **This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC.**
#### ⚠️**IMPORTANT**⚠️: When using this server, please report any Bitwarden related bug-reports or suggestions [here](https://github.com/dani-garcia/bitwarden_rs/issues/new), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels. #### ⚠️**IMPORTANT**⚠️: When using this server, please report any bugs or suggestions to us directly (look at the bottom of this page for ways to get in touch), regardless of whatever clients you are using (mobile, desktop, browser...). DO NOT use the official support channels.
--- ---
@@ -21,14 +21,14 @@ Image is based on [Rust implementation of Bitwarden API](https://github.com/dani
Basically full implementation of Bitwarden API is provided including: Basically full implementation of Bitwarden API is provided including:
* Basic single user functionality * Single user functionality
* Organizations support * Organizations support
* Attachments * Attachments
* Vault API support * Vault API support
* Serving the static files for Vault interface * Serving the static files for Vault interface
* Website icons API * Website icons API
* Authenticator and U2F support * Authenticator and U2F support
* YubiKey OTP * YubiKey and Duo support
## Installation ## Installation
Pull the docker image and mount a volume from the host for persistent storage: Pull the docker image and mount a volume from the host for persistent storage:
@@ -49,12 +49,13 @@ If you have an available domain name, you can get HTTPS certificates with [Let's
See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) for more information on how to configure and run the bitwarden_rs server. See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) for more information on how to configure and run the bitwarden_rs server.
## Get in touch ## Get in touch
To ask a question, offer suggestions or new features or to get help configuring or installing the software, please [use the forum](https://bitwardenrs.discourse.group/).
To ask a question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine. Please also report any bugs spotted here. If you spot any bugs or crashes with bitwarden_rs itself, please [create an issue](https://github.com/dani-garcia/bitwarden_rs/issues/). Make sure there aren't any similar issues open, though!
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us! If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!
### Sponsors ### Sponsors
Thanks for your contribution to the project! Thanks for your contribution to the project!
- [@Skaronator](https://github.com/Skaronator) - [@ChonoN](https://github.com/ChonoN)

View File

@@ -18,8 +18,8 @@ steps:
cargo -V cargo -V
displayName: Query rust and cargo versions displayName: Query rust and cargo versions
- script : cargo build --features "sqlite" - script : cargo test --features "sqlite"
displayName: 'Build project with sqlite backend' displayName: 'Test project with sqlite backend'
- script : cargo build --features "mysql" - script : cargo test --features "mysql"
displayName: 'Build project with mysql backend' displayName: 'Test project with mysql backend'

295
docker/Dockerfile.j2 Normal file
View File

@@ -0,0 +1,295 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
{% set build_stage_base_image = "rust:1.40" %}
{% if "alpine" in target_file %}
{% set build_stage_base_image = "clux/muslrust:nightly-2020-03-09" %}
{% set runtime_stage_base_image = "alpine:3.11" %}
{% set package_arch_name = "" %}
{% elif "amd64" in target_file %}
{% set runtime_stage_base_image = "debian:buster-slim" %}
{% set package_arch_name = "" %}
{% elif "aarch64" in target_file %}
{% set runtime_stage_base_image = "balenalib/aarch64-debian:buster" %}
{% set package_arch_name = "arm64" %}
{% elif "armv6" in target_file %}
{% set runtime_stage_base_image = "balenalib/rpi-debian:buster" %}
{% set package_arch_name = "armel" %}
{% elif "armv7" in target_file %}
{% set runtime_stage_base_image = "balenalib/armv7hf-debian:buster" %}
{% set package_arch_name = "armhf" %}
{% endif %}
{% set package_arch_prefix = ":" + package_arch_name %}
{% if package_arch_name == "" %}
{% set package_arch_prefix = "" %}
{% endif %}
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
{% set vault_image_hash = "sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c" %}
{% raw %}
# This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
# - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
# docker pull bitwardenrs/web-vault:v2.12.0e
# docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
#
# - To do the opposite, and get the tag from the hash, you can do:
# docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
{% endraw %}
FROM bitwardenrs/web-vault@{{ vault_image_hash }} as vault
########################## BUILD IMAGE ##########################
{% if "musl" in build_stage_base_image %}
# Musl build image for statically compiled binary
{% else %}
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
{% endif %}
FROM {{ build_stage_base_image }} as build
{% if "sqlite" in target_file %}
# set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite
{% elif "mysql" in target_file %}
# set mysql backend
ARG DB=mysql
{% elif "postgresql" in target_file %}
# set postgresql backend
ARG DB=postgresql
{% endif %}
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs
RUN rustup set profile minimal
{% if "alpine" in target_file %}
ENV USER "root"
{% elif "aarch64" in target_file or "armv" in target_file %}
# Install required build libs for {{ package_arch_name }} architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture {{ package_arch_name }} \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev{{ package_arch_prefix }} \
libc6-dev{{ package_arch_prefix }}
{% endif -%}
{% if "aarch64" in target_file %}
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-aarch64-linux-gnu \
&& mkdir -p ~/.cargo \
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
{% elif "armv6" in target_file %}
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabi \
&& mkdir -p ~/.cargo \
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
{% elif "armv6" in target_file %}
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
{% elif "armv7" in target_file %}
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
{% endif %}
{% if "mysql" in target_file %}
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
{% if "musl" in build_stage_base_image %}
libmysqlclient-dev{{ package_arch_prefix }} \
{% else %}
libmariadb-dev{{ package_arch_prefix }} \
{% endif %}
&& rm -rf /var/lib/apt/lists/*
{% elif "postgresql" in target_file %}
# Install PostgreSQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libpq-dev{{ package_arch_prefix }} \
&& rm -rf /var/lib/apt/lists/*
{% endif %}
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
{% if "aarch64" in target_file %}
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
{% elif "armv6" in target_file %}
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
{% elif "armv7" in target_file %}
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
{% endif -%}
{% if "alpine" in target_file %}
RUN rustup target add x86_64-unknown-linux-musl
{% elif "aarch64" in target_file %}
RUN rustup target add aarch64-unknown-linux-gnu
{% elif "armv6" in target_file %}
RUN rustup target add arm-unknown-linux-gnueabi
{% elif "armv7" in target_file %}
RUN rustup target add armv7-unknown-linux-gnueabihf
{% endif %}
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
{% if "amd64" in target_file %}
RUN cargo build --features ${DB} --release
{% elif "aarch64" in target_file %}
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
{% elif "armv6" in target_file %}
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
{% elif "armv7" in target_file %}
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
{% endif %}
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM {{ runtime_stage_base_image }}
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
{% if "alpine" in runtime_stage_base_image %}
ENV SSL_CERT_DIR=/etc/ssl/certs
{% endif %}
{% if "amd64" not in target_file %}
RUN [ "cross-build-start" ]
{% endif %}
# Install needed libraries
{% if "alpine" in runtime_stage_base_image %}
RUN apk add --no-cache \
openssl \
curl \
{% if "sqlite" in target_file %}
sqlite \
{% elif "mysql" in target_file %}
mariadb-connector-c \
{% elif "postgresql" in target_file %}
postgresql-libs \
{% endif %}
ca-certificates
{% else %}
RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \
ca-certificates \
curl \
{% if "sqlite" in target_file %}
sqlite3 \
{% elif "mysql" in target_file %}
libmariadbclient-dev \
{% elif "postgresql" in target_file %}
libpq5 \
{% endif %}
&& rm -rf /var/lib/apt/lists/*
{% endif %}
RUN mkdir /data
{% if "amd64" not in target_file %}
RUN [ "cross-build-end" ]
{% endif %}
VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
{% if "alpine" in target_file %}
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
{% elif "aarch64" in target_file %}
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
{% elif "armv6" in target_file %}
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
{% elif "armv7" in target_file %}
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
{% else %}
COPY --from=build app/target/release/bitwarden_rs .
{% endif %}
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
WORKDIR /
CMD ["/bitwarden_rs"]

9
docker/Makefile Normal file
View File

@@ -0,0 +1,9 @@
OBJECTS := $(shell find -mindepth 2 -name 'Dockerfile*')
all: $(OBJECTS)
%/Dockerfile: Dockerfile.j2 render_template
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"
%/Dockerfile.alpine: Dockerfile.j2 render_template
./render_template "$<" "{\"target_file\":\"$@\"}" > "$@"

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set mysql backend # set mysql backend
ARG DB=mysql ARG DB=mysql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for arm64 architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:arm64 \
libc6-dev:arm64
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,30 +52,42 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmariadb-dev:arm64 \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl arm64 libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture arm64 \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:arm64 \
libc6-dev:arm64 \
libmariadb-dev:arm64
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu" ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
RUN rustup target add aarch64-unknown-linux-gnu
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add aarch64-unknown-linux-gnu RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -94,6 +116,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set sqlite as default for DB ARG for backward comaptibility # set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite ARG DB=sqlite
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for arm64 architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:arm64 \
libc6-dev:arm64
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,29 +52,36 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl arm64 libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture arm64 \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:arm64 \
libc6-dev:arm64
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu" ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
RUN rustup target add aarch64-unknown-linux-gnu
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add aarch64-unknown-linux-gnu RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -93,6 +110,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

View File

@@ -1,33 +1,33 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set mysql backend # set mysql backend
ARG DB=mysql ARG DB=mysql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
@@ -38,7 +38,7 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies # Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files

View File

@@ -1,61 +1,75 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary # Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2019-11-23 as build FROM clux/muslrust:nightly-2019-12-19 as build
# set mysql backend # set mysql backend
ARG DB=mysql ARG DB=mysql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
ENV USER "root" ENV USER "root"
# Install needed libraries # Install MySQL package
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
libmysqlclient-dev \ libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
RUN rustup target add x86_64-unknown-linux-musl
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project # Make sure that we actually build the project
RUN touch src/main.rs RUN touch src/main.rs
# Build # Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
# because we already have a binary built # because we already have a binary built
FROM alpine:3.10 FROM alpine:3.11
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80 ENV ROCKET_PORT=80
@@ -65,8 +79,8 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries # Install needed libraries
RUN apk add --no-cache \ RUN apk add --no-cache \
openssl \ openssl \
mariadb-connector-c \
curl \ curl \
mariadb-connector-c \
ca-certificates ca-certificates
RUN mkdir /data RUN mkdir /data

View File

@@ -1,50 +1,44 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set mysql backend # set postgresql backend
ARG DB=postgresql ARG DB=postgresql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Using bundled SQLite, no need to install it # Install PostgreSQL package
# RUN apt-get update && apt-get install -y\
# --no-install-recommends \
# sqlite3\
# && rm -rf /var/lib/apt/lists/*
# Install MySQL package
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
libpq-dev \ libpq-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies # Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files
@@ -84,7 +78,6 @@ RUN apt-get update && apt-get install -y \
openssl \ openssl \
ca-certificates \ ca-certificates \
curl \ curl \
sqlite3 \
libpq5 \ libpq5 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@@ -1,61 +1,75 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary # Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2019-11-23 as build FROM clux/muslrust:nightly-2019-12-19 as build
# set mysql backend # set postgresql backend
ARG DB=postgresql ARG DB=postgresql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
ENV USER "root" ENV USER "root"
# Install needed libraries # Install PostgreSQL package
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
libpq-dev \ libpq-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
RUN rustup target add x86_64-unknown-linux-musl
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project # Make sure that we actually build the project
RUN touch src/main.rs RUN touch src/main.rs
# Build # Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
# because we already have a binary built # because we already have a binary built
FROM alpine:3.10 FROM alpine:3.11
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80 ENV ROCKET_PORT=80
@@ -65,9 +79,8 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries # Install needed libraries
RUN apk add --no-cache \ RUN apk add --no-cache \
openssl \ openssl \
postgresql-libs \
curl \ curl \
sqlite \ postgresql-libs \
ca-certificates ca-certificates
RUN mkdir /data RUN mkdir /data

View File

@@ -1,38 +1,38 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set sqlite as default for DB ARG for backward comaptibility # set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite ARG DB=sqlite
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Creates a dummy project used to grab dependencies # Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files # Copies over *only* your manifests and build files

View File

@@ -1,55 +1,69 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary # Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2019-11-23 as build FROM clux/muslrust:nightly-2019-12-19 as build
# set sqlite as default for DB ARG for backward comaptibility # set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite ARG DB=sqlite
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
RUN rustup target add x86_64-unknown-linux-musl
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project # Make sure that we actually build the project
RUN touch src/main.rs RUN touch src/main.rs
# Build # Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
# because we already have a binary built # because we already have a binary built
FROM alpine:3.10 FROM alpine:3.11
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80 ENV ROCKET_PORT=80
@@ -78,7 +92,6 @@ COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1 HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup! # Configures the startup!
WORKDIR / WORKDIR /
CMD ["/bitwarden_rs"] CMD ["/bitwarden_rs"]

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set mysql backend # set mysql backend
ARG DB=mysql ARG DB=mysql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for armel architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armel \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armel \
libc6-dev:armel
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,30 +52,42 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmariadb-dev:armel \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl armel libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture armel \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armel \
libc6-dev:armel \
libmariadb-dev:armel
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc" ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
RUN rustup target add arm-unknown-linux-gnueabi
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add arm-unknown-linux-gnueabi RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -94,6 +116,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set sqlite as default for DB ARG for backward comaptibility # set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite ARG DB=sqlite
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for armel architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armel \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armel \
libc6-dev:armel
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,29 +52,36 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl armel libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture armel \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armel \
libc6-dev:armel
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc" ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
RUN rustup target add arm-unknown-linux-gnueabi
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add arm-unknown-linux-gnueabi RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -93,6 +110,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set mysql backend # set mysql backend
ARG DB=mysql ARG DB=mysql
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for armhf architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armhf \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \
libc6-dev:armhf
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,31 +52,41 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmariadb-dev:armhf \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl armhf libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture armhf \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \
libc6-dev:armhf \
libmariadb-dev:armhf
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
RUN rustup target add armv7-unknown-linux-gnueabihf
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add armv7-unknown-linux-gnueabihf RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -95,6 +115,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

View File

@@ -1,36 +1,46 @@
# This file was generated using a Jinja2 template.
# Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfile's.
# Using multistage build: # Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.12.0b" # This hash is extracted from the docker web-vault builds and it's prefered over a simple tag because it's immutable.
# It can be viewed in multiple ways:
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" # - From the https://hub.docker.com/repository/docker/bitwardenrs/web-vault/tags page, click the tag name and the digest should be there.
# - From the console, with the following commands:
RUN apk add --no-cache --upgrade \ # docker pull bitwardenrs/web-vault:v2.12.0e
curl \ # docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.12.0e
tar #
# - To do the opposite, and get the tag from the hash, you can do:
RUN mkdir /web-vault # docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c
WORKDIR /web-vault FROM bitwardenrs/web-vault@sha256:ce56b3f5e538351411785ac45e9b4b913259c3508b1323d62e8fa0f30717dd1c as vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust:1.39 as build FROM rust:1.40 as build
# set sqlite as default for DB ARG for backward comaptibility # set sqlite as default for DB ARG for backward compatibility
ARG DB=sqlite ARG DB=sqlite
# Build time options to avoid dpkg warnings and help with reproducible builds.
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 TZ=UTC TERM=xterm-256color
# Don't download rust docs # Don't download rust docs
RUN rustup set profile minimal RUN rustup set profile minimal
# Install required build libs for armhf architecture.
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armhf \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \
libc6-dev:armhf
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \ --no-install-recommends \
@@ -42,29 +52,35 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin /app
WORKDIR /app WORKDIR /app
# Prepare openssl armhf libs # Copies over *only* your manifests and build files
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ COPY ./Cargo.* ./
/etc/apt/sources.list.d/deb-src.list \ COPY ./rust-toolchain ./rust-toolchain
&& dpkg --add-architecture armhf \ COPY ./build.rs ./build.rs
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \
libc6-dev:armhf
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
RUN rustup target add armv7-unknown-linux-gnueabihf
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Build # Make sure that we actually build the project
RUN rustup target add armv7-unknown-linux-gnueabihf RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -93,6 +109,7 @@ RUN [ "cross-build-end" ]
VOLUME /data VOLUME /data
EXPOSE 80 EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault) # Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage # and the binary from the "build" stage to the current stage

17
docker/render_template Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
import os, argparse, json
import jinja2
args_parser = argparse.ArgumentParser()
args_parser.add_argument('template_file', help='Jinja2 template file to render.')
args_parser.add_argument('render_vars', help='JSON-encoded data to pass to the templating engine.')
cli_args = args_parser.parse_args()
render_vars = json.loads(cli_args.render_vars)
environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.getcwd()),
trim_blocks=True,
)
print(environment.get_template(cli_args.template_file).render(render_vars))

View File

@@ -1 +1 @@
nightly-2019-11-17 nightly-2020-03-09

View File

@@ -1 +1,2 @@
version = "Two"
max_width = 120 max_width = 120

View File

@@ -1,3 +1,4 @@
use once_cell::sync::Lazy;
use serde_json::Value; use serde_json::Value;
use std::process::Command; use std::process::Command;
@@ -26,6 +27,7 @@ pub fn routes() -> Vec<Route> {
post_admin_login, post_admin_login,
admin_page, admin_page,
invite_user, invite_user,
logout,
delete_user, delete_user,
deauth_user, deauth_user,
remove_2fa, remove_2fa,
@@ -33,12 +35,12 @@ pub fn routes() -> Vec<Route> {
post_config, post_config,
delete_config, delete_config,
backup_db, backup_db,
test_smtp,
] ]
} }
lazy_static! { static CAN_BACKUP: Lazy<bool> =
static ref CAN_BACKUP: bool = cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok(); Lazy::new(|| cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok());
}
#[get("/")] #[get("/")]
fn admin_disabled() -> &'static str { fn admin_disabled() -> &'static str {
@@ -51,11 +53,15 @@ const ADMIN_PATH: &str = "/admin";
const BASE_TEMPLATE: &str = "admin/base"; const BASE_TEMPLATE: &str = "admin/base";
const VERSION: Option<&str> = option_env!("GIT_VERSION"); const VERSION: Option<&str> = option_env!("GIT_VERSION");
fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
}
#[get("/", rank = 2)] #[get("/", rank = 2)]
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
// If there is an error, show it // If there is an error, show it
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg}); let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg, "urlpath": CONFIG.domain_path()});
// Return the page // Return the page
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
@@ -75,7 +81,7 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
if !_validate_token(&data.token) { if !_validate_token(&data.token) {
error!("Invalid admin token. IP: {}", ip.ip); error!("Invalid admin token. IP: {}", ip.ip);
Err(Flash::error( Err(Flash::error(
Redirect::to(ADMIN_PATH), Redirect::to(admin_path()),
"Invalid admin token, please try again.", "Invalid admin token, please try again.",
)) ))
} else { } else {
@@ -84,14 +90,14 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
let jwt = encode_jwt(&claims); let jwt = encode_jwt(&claims);
let cookie = Cookie::build(COOKIE_NAME, jwt) let cookie = Cookie::build(COOKIE_NAME, jwt)
.path(ADMIN_PATH) .path(admin_path())
.max_age(chrono::Duration::minutes(20)) .max_age(chrono::Duration::minutes(20))
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.http_only(true) .http_only(true)
.finish(); .finish();
cookies.add(cookie); cookies.add(cookie);
Ok(Redirect::to(ADMIN_PATH)) Ok(Redirect::to(admin_path()))
} }
} }
@@ -109,6 +115,8 @@ struct AdminTemplateData {
users: Vec<Value>, users: Vec<Value>,
config: Value, config: Value,
can_backup: bool, can_backup: bool,
logged_in: bool,
urlpath: String,
} }
impl AdminTemplateData { impl AdminTemplateData {
@@ -119,6 +127,8 @@ impl AdminTemplateData {
users, users,
config: CONFIG.prepare_json(), config: CONFIG.prepare_json(),
can_backup: *CAN_BACKUP, can_backup: *CAN_BACKUP,
logged_in: true,
urlpath: CONFIG.domain_path(),
} }
} }
@@ -150,22 +160,35 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
err!("User already exists") err!("User already exists")
} }
if !CONFIG.invitations_allowed() {
err!("Invitations are not allowed")
}
let mut user = User::new(email); let mut user = User::new(email);
user.save(&conn)?; user.save(&conn)?;
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
let org_name = "bitwarden_rs"; mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None)
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
} else { } else {
let invitation = Invitation::new(data.email); let invitation = Invitation::new(data.email);
invitation.save(&conn) invitation.save(&conn)
} }
} }
#[post("/test/smtp", data = "<data>")]
fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
let data: InviteData = data.into_inner();
let email = data.email.clone();
if CONFIG.mail_enabled() {
mail::send_test(&email)
} else {
err!("Mail is not enabled")
}
}
#[get("/logout")]
fn logout(mut cookies: Cookies) -> Result<Redirect, ()> {
cookies.remove(Cookie::named(COOKIE_NAME));
Ok(Redirect::to(admin_path()))
}
#[get("/users")] #[get("/users")]
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult { fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
let users = User::get_all(&conn); let users = User::get_all(&conn);

View File

@@ -1,13 +1,13 @@
use rocket_contrib::json::Json;
use chrono::Utc; use chrono::Utc;
use rocket_contrib::json::Json;
use crate::db::models::*; use crate::db::models::*;
use crate::db::DbConn; use crate::db::DbConn;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType}; use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType};
use crate::auth::{decode_invite, decode_delete, decode_verify_email, Headers}; use crate::auth::{decode_delete, decode_invite, decode_verify_email, Headers};
use crate::mail;
use crate::crypto; use crate::crypto;
use crate::mail;
use crate::CONFIG; use crate::CONFIG;
@@ -414,20 +414,21 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
match user.email_new { match user.email_new {
Some(ref val) => { Some(ref val) => {
if *val != data.NewEmail.to_string() { if val != &data.NewEmail {
err!("Email change mismatch"); err!("Email change mismatch");
} }
}, }
None => err!("No email change pending"), None => err!("No email change pending"),
} }
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
// Only check the token if we sent out an email... // Only check the token if we sent out an email...
match user.email_new_token { match user.email_new_token {
Some(ref val) => Some(ref val) => {
if *val != data.Token.into_string() { if *val != data.Token.into_string() {
err!("Token mismatch"); err!("Token mismatch");
} }
}
None => err!("No email change pending"), None => err!("No email change pending"),
} }
user.verified_at = Some(Utc::now().naive_utc()); user.verified_at = Some(Utc::now().naive_utc());
@@ -480,11 +481,9 @@ fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn)
Ok(claims) => claims, Ok(claims) => claims,
Err(_) => err!("Invalid claim"), Err(_) => err!("Invalid claim"),
}; };
if claims.sub != user.uuid { if claims.sub != user.uuid {
err!("Invalid claim"); err!("Invalid claim");
} }
user.verified_at = Some(Utc::now().naive_utc()); user.verified_at = Some(Utc::now().naive_utc());
user.last_verifying_at = None; user.last_verifying_at = None;
user.login_verify_count = 0; user.login_verify_count = 0;
@@ -543,11 +542,9 @@ fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, conn: DbC
Ok(claims) => claims, Ok(claims) => claims,
Err(_) => err!("Invalid claim"), Err(_) => err!("Invalid claim"),
}; };
if claims.sub != user.uuid { if claims.sub != user.uuid {
err!("Invalid claim"); err!("Invalid claim");
} }
user.delete(&conn) user.delete(&conn)
} }

View File

@@ -642,20 +642,49 @@ fn post_attachment(
) -> JsonResult { ) -> JsonResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) { let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher, Some(cipher) => cipher,
None => err!("Cipher doesn't exist"), None => err_discard!("Cipher doesn't exist", data),
}; };
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) { if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher is not write accessible") err_discard!("Cipher is not write accessible", data)
} }
let mut params = content_type.params(); let mut params = content_type.params();
let boundary_pair = params.next().expect("No boundary provided"); let boundary_pair = params.next().expect("No boundary provided");
let boundary = boundary_pair.1; let boundary = boundary_pair.1;
let size_limit = if let Some(ref user_uuid) = cipher.user_uuid {
match CONFIG.user_attachment_limit() {
Some(0) => err_discard!("Attachments are disabled", data),
Some(limit) => {
let left = limit - Attachment::size_by_user(user_uuid, &conn);
if left <= 0 {
err_discard!("Attachment size limit reached! Delete some files to open space", data)
}
Some(left as u64)
}
None => None,
}
} else if let Some(ref org_uuid) = cipher.organization_uuid {
match CONFIG.org_attachment_limit() {
Some(0) => err_discard!("Attachments are disabled", data),
Some(limit) => {
let left = limit - Attachment::size_by_org(org_uuid, &conn);
if left <= 0 {
err_discard!("Attachment size limit reached! Delete some files to open space", data)
}
Some(left as u64)
}
None => None,
}
} else {
err_discard!("Cipher is neither owned by a user nor an organization", data);
};
let base_path = Path::new(&CONFIG.attachments_folder()).join(&cipher.uuid); let base_path = Path::new(&CONFIG.attachments_folder()).join(&cipher.uuid);
let mut attachment_key = None; let mut attachment_key = None;
let mut error = None;
Multipart::with_body(data.open(), boundary) Multipart::with_body(data.open(), boundary)
.foreach_entry(|mut field| { .foreach_entry(|mut field| {
@@ -674,18 +703,21 @@ fn post_attachment(
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10])); let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name); let path = base_path.join(&file_name);
let size = match field.data.save().memory_threshold(0).size_limit(None).with_path(path) { let size = match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) {
SaveResult::Full(SavedData::File(_, size)) => size as i32, SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => { SaveResult::Full(other) => {
error!("Attachment is not a file: {:?}", other); std::fs::remove_file(path).ok();
error = Some(format!("Attachment is not a file: {:?}", other));
return; return;
} }
SaveResult::Partial(_, reason) => { SaveResult::Partial(_, reason) => {
error!("Partial result: {:?}", reason); std::fs::remove_file(path).ok();
error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason));
return; return;
} }
SaveResult::Error(e) => { SaveResult::Error(e) => {
error!("Error: {:?}", e); std::fs::remove_file(path).ok();
error = Some(format!("Error: {:?}", e));
return; return;
} }
}; };
@@ -699,6 +731,10 @@ fn post_attachment(
}) })
.expect("Error processing multipart data"); .expect("Error processing multipart data");
if let Some(ref e) = error {
err!(e);
}
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn)); nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))

View File

@@ -149,11 +149,10 @@ fn hibp_breach(username: String) -> JsonResult {
use reqwest::{header::USER_AGENT, Client}; use reqwest::{header::USER_AGENT, Client};
if let Some(api_key) = crate::CONFIG.hibp_api_key() { if let Some(api_key) = crate::CONFIG.hibp_api_key() {
let hibp_client = Client::builder() let hibp_client = Client::builder().use_sys_proxy().build()?;
.use_sys_proxy()
.build()?;
let res = hibp_client.get(&url) let res = hibp_client
.get(&url)
.header(USER_AGENT, user_agent) .header(USER_AGENT, user_agent)
.header("hibp-api-key", api_key) .header("hibp-api-key", api_key)
.send()?; .send()?;
@@ -173,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult {
"BreachDate": "2019-08-18T00:00:00Z", "BreachDate": "2019-08-18T00:00:00Z",
"AddedDate": "2019-08-18T00:00:00Z", "AddedDate": "2019-08-18T00:00:00Z",
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username), "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
"LogoPath": "/bwrs_static/hibp.png", "LogoPath": "bwrs_static/hibp.png",
"PwnCount": 0, "PwnCount": 0,
"DataClasses": [ "DataClasses": [
"Error - No API key set!" "Error - No API key set!"

View File

@@ -103,7 +103,6 @@ pub fn validate_totp_code_str(user_uuid: &str, totp_code: &str, secret: &str, co
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult { pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult {
use oath::{totp_raw_custom_time, HashType}; use oath::{totp_raw_custom_time, HashType};
use std::time::{UNIX_EPOCH, SystemTime};
let decoded_secret = match BASE32.decode(secret.as_bytes()) { let decoded_secret = match BASE32.decode(secret.as_bytes()) {
Ok(s) => s, Ok(s) => s,
@@ -116,24 +115,23 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &
}; };
// Get the current system time in UNIX Epoch (UTC) // Get the current system time in UNIX Epoch (UTC)
let current_time: u64 = SystemTime::now().duration_since(UNIX_EPOCH) let current_time = chrono::Utc::now();
.expect("Earlier than 1970-01-01 00:00:00 UTC").as_secs(); let current_timestamp = current_time.timestamp();
// The amount of steps back and forward in time // The amount of steps back and forward in time
// Also check if we need to disable time drifted TOTP codes. // Also check if we need to disable time drifted TOTP codes.
// If that is the case, we set the steps to 0 so only the current TOTP is valid. // If that is the case, we set the steps to 0 so only the current TOTP is valid.
let steps = if CONFIG.authenticator_disable_time_drift() { 0 } else { 1 }; let steps: i64 = if CONFIG.authenticator_disable_time_drift() { 0 } else { 1 };
for step in -steps..=steps { for step in -steps..=steps {
let time_step = (current_time / 30) as i32 + step; let time_step = current_timestamp / 30i64 + step;
// We need to calculate the time offsite and cast it as an i128. // We need to calculate the time offsite and cast it as an i128.
// Else we can't do math with it on a default u64 variable. // Else we can't do math with it on a default u64 variable.
let time_offset: i128 = (step * 30).into(); let time = (current_timestamp + step * 30i64) as u64;
let generated = totp_raw_custom_time(&decoded_secret, 6, 0, 30, (current_time as i128 + time_offset) as u64, &HashType::SHA1); let generated = totp_raw_custom_time(&decoded_secret, 6, 0, 30, time, &HashType::SHA1);
// Check the the given code equals the generated and if the time_step is larger then the one last used. // Check the the given code equals the generated and if the time_step is larger then the one last used.
if generated == totp_code && time_step > twofactor.last_used { if generated == totp_code && time_step > twofactor.last_used as i64 {
// If the step does not equals 0 the time is drifted either server or client side. // If the step does not equals 0 the time is drifted either server or client side.
if step != 0 { if step != 0 {
info!("TOTP Time drift detected. The step offset is {}", step); info!("TOTP Time drift detected. The step offset is {}", step);
@@ -141,15 +139,15 @@ pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &
// Save the last used time step so only totp time steps higher then this one are allowed. // Save the last used time step so only totp time steps higher then this one are allowed.
// This will also save a newly created twofactor if the code is correct. // This will also save a newly created twofactor if the code is correct.
twofactor.last_used = time_step; twofactor.last_used = time_step as i32;
twofactor.save(&conn)?; twofactor.save(&conn)?;
return Ok(()); return Ok(());
} else if generated == totp_code && time_step <= twofactor.last_used { } else if generated == totp_code && time_step <= twofactor.last_used as i64 {
warn!("This or a TOTP code within {} steps back and forward has already been used!", steps); warn!("This or a TOTP code within {} steps back and forward has already been used!", steps);
err!("Invalid TOTP Code!"); err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
} }
} }
// Else no valide code received, deny access // Else no valide code received, deny access
err!("Invalid TOTP code!"); err!(format!("Invalid TOTP code! Server time: {}", current_time.format("%F %T UTC")));
} }

View File

@@ -16,11 +16,7 @@ use crate::error::MapResult;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![get_duo, activate_duo, activate_duo_put,]
get_duo,
activate_duo,
activate_duo_put,
]
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View File

@@ -18,12 +18,7 @@ use chrono::{Duration, NaiveDateTime, Utc};
use std::ops::Add; use std::ops::Add;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![get_email, send_email_login, send_email, email,]
get_email,
send_email_login,
send_email,
email,
]
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -276,10 +271,10 @@ impl EmailTokenData {
/// Takes an email address and obscures it by replacing it with asterisks except two characters. /// Takes an email address and obscures it by replacing it with asterisks except two characters.
pub fn obscure_email(email: &str) -> String { pub fn obscure_email(email: &str) -> String {
let split: Vec<&str> = email.split('@').collect(); let split: Vec<&str> = email.rsplitn(2, '@').collect();
let mut name = split[0].to_string(); let mut name = split[1].to_string();
let domain = &split[1]; let domain = &split[0];
let name_size = name.chars().count(); let name_size = name.chars().count();
@@ -321,14 +316,14 @@ mod tests {
#[test] #[test]
fn test_token() { fn test_token() {
let result = generate_token(19).unwrap(); let result = crypto::generate_token(19).unwrap();
assert_eq!(result.chars().count(), 19); assert_eq!(result.chars().count(), 19);
} }
#[test] #[test]
fn test_token_too_large() { fn test_token_too_large() {
let result = generate_token(20); let result = crypto::generate_token(20);
assert!(result.is_err(), "too large token should give an error"); assert!(result.is_err(), "too large token should give an error");
} }

View File

@@ -1,3 +1,4 @@
use once_cell::sync::Lazy;
use rocket::Route; use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
@@ -18,10 +19,8 @@ use crate::CONFIG;
const U2F_VERSION: &str = "U2F_V2"; const U2F_VERSION: &str = "U2F_V2";
lazy_static! { static APP_ID: Lazy<String> = Lazy::new(|| format!("{}/app-id.json", &CONFIG.domain()));
static ref APP_ID: String = format!("{}/app-id.json", &CONFIG.domain()); static U2F: Lazy<U2f> = Lazy::new(|| U2f::new(APP_ID.clone()));
static ref U2F: U2f = U2f::new(APP_ID.clone());
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![
@@ -29,6 +28,7 @@ pub fn routes() -> Vec<Route> {
generate_u2f_challenge, generate_u2f_challenge,
activate_u2f, activate_u2f,
activate_u2f_put, activate_u2f_put,
delete_u2f,
] ]
} }
@@ -91,6 +91,7 @@ struct RegistrationDef {
key_handle: Vec<u8>, key_handle: Vec<u8>,
pub_key: Vec<u8>, pub_key: Vec<u8>,
attestation_cert: Option<Vec<u8>>, attestation_cert: Option<Vec<u8>>,
device_name: Option<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -194,6 +195,50 @@ fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbC
activate_u2f(data, headers, conn) activate_u2f(data, headers, conn)
} }
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct DeleteU2FData {
Id: NumberOrString,
MasterPasswordHash: String,
}
#[delete("/two-factor/u2f", data = "<data>")]
fn delete_u2f(data: JsonUpcase<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: DeleteU2FData = data.into_inner().data;
let id = data.Id.into_i32()?;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let type_ = TwoFactorType::U2f as i32;
let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn) {
Some(tf) => tf,
None => err!("U2F data not found!"),
};
let mut data: Vec<U2FRegistration> = match serde_json::from_str(&tf.data) {
Ok(d) => d,
Err(_) => err!("Error parsing U2F data"),
};
data.retain(|r| r.id != id);
let new_data_str = serde_json::to_string(&data)?;
tf.data = new_data_str;
tf.save(&conn)?;
let keys_json: Vec<Value> = data.iter().map(U2FRegistration::to_json).collect();
Ok(Json(json!({
"Enabled": true,
"Keys": keys_json,
"Object": "twoFactorU2f"
})))
}
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge { fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
let challenge = U2F.generate_challenge().unwrap(); let challenge = U2F.generate_challenge().unwrap();

View File

@@ -16,11 +16,7 @@ use crate::error::{Error, MapResult};
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
generate_yubikey,
activate_yubikey,
activate_yubikey_put,
]
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]

View File

@@ -1,3 +1,4 @@
use once_cell::sync::Lazy;
use std::fs::{create_dir_all, remove_file, symlink_metadata, File}; use std::fs::{create_dir_all, remove_file, symlink_metadata, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
@@ -16,6 +17,7 @@ use soup::prelude::*;
use crate::error::Error; use crate::error::Error;
use crate::CONFIG; use crate::CONFIG;
use crate::util::Cached;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![icon] routes![icon]
@@ -25,16 +27,16 @@ const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
const ALLOWED_CHARS: &str = "_-."; const ALLOWED_CHARS: &str = "_-.";
lazy_static! { static CLIENT: Lazy<Client> = Lazy::new(|| {
// Reuse the client between requests // Reuse the client between requests
static ref CLIENT: Client = Client::builder() Client::builder()
.use_sys_proxy() .use_sys_proxy()
.gzip(true) .gzip(true)
.timeout(Duration::from_secs(CONFIG.icon_download_timeout())) .timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
.default_headers(_header_map()) .default_headers(_header_map())
.build() .build()
.unwrap(); .unwrap()
} });
fn is_valid_domain(domain: &str) -> bool { fn is_valid_domain(domain: &str) -> bool {
// Don't allow empty or too big domains or path traversal // Don't allow empty or too big domains or path traversal
@@ -53,15 +55,15 @@ fn is_valid_domain(domain: &str) -> bool {
} }
#[get("/<domain>/icon.png")] #[get("/<domain>/icon.png")]
fn icon(domain: String) -> Content<Vec<u8>> { fn icon(domain: String) -> Cached<Content<Vec<u8>>> {
let icon_type = ContentType::new("image", "x-icon"); let icon_type = ContentType::new("image", "x-icon");
if !is_valid_domain(&domain) { if !is_valid_domain(&domain) {
warn!("Invalid domain: {:#?}", domain); warn!("Invalid domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec()); return Cached::long(Content(icon_type, FALLBACK_ICON.to_vec()));
} }
Content(icon_type, get_icon(&domain)) Cached::long(Content(icon_type, get_icon(&domain)))
} }
fn check_icon_domain_is_blacklisted(domain: &str) -> bool { fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
@@ -213,7 +215,7 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
let mut cookie_str = String::new(); let mut cookie_str = String::new();
let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain)); let resp = get_page(&ssldomain).or_else(|_| get_page(&httpdomain));
if let Ok(content) = resp { if let Ok(mut content) = resp {
// Extract the URL from the respose in case redirects occured (like @ gitlab.com) // Extract the URL from the respose in case redirects occured (like @ gitlab.com)
let url = content.url().clone(); let url = content.url().clone();
@@ -233,7 +235,11 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
// Add the default favicon.ico to the list with the domain the content responded from. // Add the default favicon.ico to the list with the domain the content responded from.
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string())); iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
let soup = Soup::from_reader(content)?; // 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 = crate::util::LimitedReader::new(&mut content, 512 * 1024);
let soup = Soup::from_reader(limited_reader)?;
// Search for and filter // Search for and filter
let favicons = soup let favicons = soup
.tag("link") .tag("link")
@@ -387,7 +393,7 @@ fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
break; break;
} }
} }
_ => warn!("data uri is invalid") _ => warn!("data uri is invalid"),
}; };
} else { } else {
match get_page_with_cookies(&icon.href, &cookie_str) { match get_page_with_cookies(&icon.href, &cookie_str) {

View File

@@ -1,9 +1,9 @@
use chrono::Utc;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::request::{Form, FormItems, FromForm}; use rocket::request::{Form, FormItems, FromForm};
use rocket::Route; use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use chrono::Utc;
use crate::api::core::two_factor::email::EmailTokenData; use crate::api::core::two_factor::email::EmailTokenData;
use crate::api::core::two_factor::{duo, email, yubikey}; use crate::api::core::two_factor::{duo, email, yubikey};
@@ -97,7 +97,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
) )
} }
if !user.verified_at.is_some() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 { if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 {
let resend_limit = CONFIG.signups_verify_resend_limit() as i32; let resend_limit = CONFIG.signups_verify_resend_limit() as i32;
@@ -106,7 +106,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
// their email address, and we haven't sent them a reminder in a while... // their email address, and we haven't sent them a reminder in a while...
let mut user = user; let mut user = user;
user.last_verifying_at = Some(now); user.last_verifying_at = Some(now);
user.login_verify_count = user.login_verify_count + 1; user.login_verify_count += 1;
if let Err(e) = user.save(&conn) { if let Err(e) = user.save(&conn) {
error!("Error updating user: {:#?}", e); error!("Error updating user: {:#?}", e);
@@ -211,7 +211,7 @@ fn twofactor_auth(
let twofactor_code = match data.two_factor_token { let twofactor_code = match data.two_factor_token {
Some(ref code) => code, Some(ref code) => code,
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?), None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA token not provided"),
}; };
let selected_twofactor = twofactors let selected_twofactor = twofactors
@@ -237,7 +237,7 @@ fn twofactor_auth(
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
} }
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?), _ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?, "2FA Remember token not provided"),
} }
} }
_ => err!("Invalid two factor provider"), _ => err!("Invalid two factor provider"),

View File

@@ -1,20 +1,31 @@
use std::sync::atomic::{AtomicBool, Ordering};
use rocket::Route; use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use crate::api::JsonResult; use crate::api::{EmptyResult, JsonResult};
use crate::auth::Headers; use crate::auth::Headers;
use crate::db::DbConn; use crate::db::DbConn;
use crate::CONFIG; use crate::{Error, CONFIG};
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err] routes![negotiate, websockets_err]
} }
static SHOW_WEBSOCKETS_MSG: AtomicBool = AtomicBool::new(true);
#[get("/hub")] #[get("/hub")]
fn websockets_err() -> JsonResult { fn websockets_err() -> EmptyResult {
err!("'/notifications/hub' should be proxied to the websocket server or notifications won't work. Go to the README for more info.") if CONFIG.websocket_enabled() && SHOW_WEBSOCKETS_MSG.compare_and_swap(true, false, Ordering::Relaxed) {
err!("###########################################################
'/notifications/hub' should be proxied to the websocket server or notifications won't work.
Go to the Wiki for more info, or disable WebSockets setting WEBSOCKET_ENABLED=false.
###########################################################################################")
} else {
Err(Error::empty())
}
} }
#[post("/hub/negotiate")] #[post("/hub/negotiate")]
@@ -43,10 +54,11 @@ fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
// //
// Websockets server // Websockets server
// //
use std::io;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender, WebSocket}; use ws::{self, util::Token, Factory, Handler, Handshake, Message, Sender};
use chashmap::CHashMap; use chashmap::CHashMap;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@@ -124,20 +136,51 @@ struct InitialMessage {
const PING_MS: u64 = 15_000; const PING_MS: u64 = 15_000;
const PING: Token = Token(1); const PING: Token = Token(1);
const ID_KEY: &str = "id=";
const ACCESS_TOKEN_KEY: &str = "access_token=";
impl WSHandler {
fn err(&self, msg: &'static str) -> ws::Result<()> {
self.out.close(ws::CloseCode::Invalid)?;
// We need to specifically return an IO error so ws closes the connection
let io_error = io::Error::from(io::ErrorKind::InvalidData);
Err(ws::Error::new(ws::ErrorKind::Io(io_error), msg))
}
}
impl Handler for WSHandler { impl Handler for WSHandler {
fn on_open(&mut self, hs: Handshake) -> ws::Result<()> { fn on_open(&mut self, hs: Handshake) -> ws::Result<()> {
// TODO: Improve this split // Path == "/notifications/hub?id=<id>==&access_token=<access_token>"
let path = hs.request.resource(); let path = hs.request.resource();
let mut query_split: Vec<_> = path.split('?').nth(1).unwrap().split('&').collect();
query_split.sort(); let (_id, access_token) = match path.split('?').nth(1) {
let access_token = &query_split[0][13..]; Some(params) => {
let _id = &query_split[1][3..]; let mut params_iter = params.split('&').take(2);
let mut id = None;
let mut access_token = None;
while let Some(val) = params_iter.next() {
if val.starts_with(ID_KEY) {
id = Some(&val[ID_KEY.len()..]);
} else if val.starts_with(ACCESS_TOKEN_KEY) {
access_token = Some(&val[ACCESS_TOKEN_KEY.len()..]);
}
}
match (id, access_token) {
(Some(a), Some(b)) => (a, b),
_ => return self.err("Missing id or access token"),
}
}
None => return self.err("Missing query path"),
};
// Validate the user // Validate the user
use crate::auth; use crate::auth;
let claims = match auth::decode_login(access_token) { let claims = match auth::decode_login(access_token) {
Ok(claims) => claims, Ok(claims) => claims,
Err(_) => return Err(ws::Error::new(ws::ErrorKind::Internal, "Invalid access token provided")), Err(_) => return self.err("Invalid access token provided"),
}; };
// Assign the user to the handler // Assign the user to the handler
@@ -179,10 +222,7 @@ impl Handler for WSHandler {
// reschedule the timeout // reschedule the timeout
self.out.timeout(PING_MS, PING) self.out.timeout(PING_MS, PING)
} else { } else {
Err(ws::Error::new( Ok(())
ws::ErrorKind::Internal,
"Invalid timeout token provided",
))
} }
} }
} }
@@ -351,7 +391,14 @@ pub fn start_notification_server() -> WebSocketUsers {
if CONFIG.websocket_enabled() { if CONFIG.websocket_enabled() {
thread::spawn(move || { thread::spawn(move || {
WebSocket::new(factory) let mut settings = ws::Settings::default();
settings.max_connections = 500;
settings.queue_size = 2;
settings.panic_on_internal = false;
ws::Builder::new()
.with_settings(settings)
.build(factory)
.unwrap() .unwrap()
.listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port())) .listen((CONFIG.websocket_address().as_str(), CONFIG.websocket_port()))
.unwrap(); .unwrap();

View File

@@ -7,11 +7,13 @@ use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use crate::util::Cached;
use crate::error::Error; use crate::error::Error;
use crate::util::Cached;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { 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() { if CONFIG.web_vault_enabled() {
routes![web_index, app_id, web_files, attachments, alive, static_files] routes![web_index, app_id, web_files, attachments, alive, static_files]
} else { } else {
@@ -21,9 +23,7 @@ pub fn routes() -> Vec<Route> {
#[get("/")] #[get("/")]
fn web_index() -> Cached<Option<NamedFile>> { fn web_index() -> Cached<Option<NamedFile>> {
Cached::short(NamedFile::open( Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).ok())
Path::new(&CONFIG.web_vault_folder()).join("index.html"),
).ok())
} }
#[get("/app-id.json")] #[get("/app-id.json")]
@@ -37,7 +37,17 @@ fn app_id() -> Cached<Content<Json<Value>>> {
{ {
"version": { "major": 1, "minor": 0 }, "version": { "major": 1, "minor": 0 },
"ids": [ "ids": [
&CONFIG.domain(), // Per <https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-the-facetid-of-a-calling-application>:
//
// "In the Web case, the FacetID MUST be the Web Origin [RFC6454]
// of the web page triggering the FIDO operation, written as
// a URI with an empty path. Default ports are omitted and any
// path component is ignored."
//
// This leaves it unclear as to whether the path must be empty,
// or whether it can be non-empty and will be ignored. To be on
// the safe side, use a proper web origin (with empty path).
&CONFIG.domain_origin(),
"ios:bundle-id:com.8bit.bitwarden", "ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ] "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
}] }]
@@ -75,6 +85,6 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
"bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))), "bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))),
"md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))), "md5.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/md5.js"))),
"identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))), "identicon.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/identicon.js"))),
_ => err!("Image not found"), _ => err!(format!("Static file not found: {}", filename)),
} }
} }

View File

@@ -3,6 +3,7 @@
// //
use crate::util::read_file; use crate::util::read_file;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use once_cell::sync::Lazy;
use jsonwebtoken::{self, Algorithm, Header}; use jsonwebtoken::{self, Algorithm, Header};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@@ -13,23 +14,21 @@ use crate::CONFIG;
const JWT_ALGORITHM: Algorithm = Algorithm::RS256; const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
lazy_static! { pub static DEFAULT_VALIDITY: Lazy<Duration> = Lazy::new(|| Duration::hours(2));
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2); static JWT_HEADER: Lazy<Header> = Lazy::new(|| Header::new(JWT_ALGORITHM));
static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM); pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CONFIG.domain_origin()));
pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain()); static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin()));
pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain()); static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin()));
pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain()); static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain()); static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain()); static PRIVATE_RSA_KEY: Lazy<Vec<u8>> = Lazy::new(|| match read_file(&CONFIG.private_rsa_key()) {
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
Ok(key) => key, Ok(key) => key,
Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e), Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
}; });
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) { static PUBLIC_RSA_KEY: Lazy<Vec<u8>> = Lazy::new(|| match read_file(&CONFIG.public_rsa_key()) {
Ok(key) => key, Ok(key) => key,
Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e), Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
}; });
}
pub fn encode_jwt<T: Serialize>(claims: &T) -> String { pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) { match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
@@ -156,9 +155,7 @@ pub struct DeleteJWTClaims {
pub sub: String, pub sub: String,
} }
pub fn generate_delete_claims( pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims {
uuid: String,
) -> DeleteJWTClaims {
let time_now = Utc::now().naive_utc(); let time_now = Utc::now().naive_utc();
DeleteJWTClaims { DeleteJWTClaims {
nbf: time_now.timestamp(), nbf: time_now.timestamp(),
@@ -180,9 +177,7 @@ pub struct VerifyEmailJWTClaims {
pub sub: String, pub sub: String,
} }
pub fn generate_verify_email_claims( pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims {
uuid: String,
) -> DeleteJWTClaims {
let time_now = Utc::now().naive_utc(); let time_now = Utc::now().naive_utc();
DeleteJWTClaims { DeleteJWTClaims {
nbf: time_now.timestamp(), nbf: time_now.timestamp(),
@@ -430,12 +425,25 @@ pub struct ClientIp {
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp { impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let ip = match request.client_ip() { let ip = if CONFIG._ip_header_enabled() {
Some(addr) => addr, req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| {
None => "0.0.0.0".parse().unwrap(), match ip.find(',') {
Some(idx) => &ip[..idx],
None => ip,
}
.parse()
.map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip))
.ok()
})
} else {
None
}; };
let ip = ip
.or_else(|| req.remote().map(|r| r.ip()))
.unwrap_or_else(|| "0.0.0.0".parse().unwrap());
Outcome::Success(ClientIp { ip }) Outcome::Success(ClientIp { ip })
} }
} }

View File

@@ -1,19 +1,23 @@
use once_cell::sync::Lazy;
use std::process::exit; use std::process::exit;
use std::sync::RwLock; use std::sync::RwLock;
use crate::error::Error; use reqwest::Url;
use crate::util::get_env;
lazy_static! { use crate::error::Error;
pub static ref CONFIG: Config = Config::load().unwrap_or_else(|e| { use crate::util::{get_env, get_env_bool};
println!("Error loading config:\n\t{:?}\n", e);
exit(12) static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
});
pub static ref CONFIG_FILE: String = {
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data")); let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder)) get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder))
}; });
}
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::load().unwrap_or_else(|e| {
println!("Error loading config:\n\t{:?}\n", e);
exit(12)
})
});
pub type Pass = String; pub type Pass = String;
@@ -23,13 +27,13 @@ macro_rules! make_config {
$group:ident $(: $group_enabled:ident)? { $group:ident $(: $group_enabled:ident)? {
$( $(
$(#[doc = $doc:literal])+ $(#[doc = $doc:literal])+
$name:ident : $ty:ty, $editable:literal, $none_action:ident $(, $default:expr)?; $name:ident : $ty:ident, $editable:literal, $none_action:ident $(, $default:expr)?;
)+}, )+},
)+) => { )+) => {
pub struct Config { inner: RwLock<Inner> } pub struct Config { inner: RwLock<Inner> }
struct Inner { struct Inner {
templates: Handlebars, templates: Handlebars<'static>,
config: ConfigItems, config: ConfigItems,
_env: ConfigBuilder, _env: ConfigBuilder,
@@ -50,7 +54,7 @@ macro_rules! make_config {
let mut builder = ConfigBuilder::default(); let mut builder = ConfigBuilder::default();
$($( $($(
builder.$name = get_env(&stringify!($name).to_uppercase()); builder.$name = make_config! { @getenv &stringify!($name).to_uppercase(), $ty };
)+)+ )+)+
builder builder
@@ -185,19 +189,28 @@ macro_rules! make_config {
} }
} }
}}; }};
( @build $value:expr, $config:expr, gen, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}};
( @getenv $name:expr, bool ) => { get_env_bool($name) };
( @getenv $name:expr, $ty:ident ) => { get_env($name) };
} }
//STRUCTURE: //STRUCTURE:
// /// Short description (without this they won't appear on the list) // /// Short description (without this they won't appear on the list)
// group { // group {
// /// Friendly Name |> Description (Optional) // /// Friendly Name |> Description (Optional)
// name: type, is_editable, none_action, <default_value (Optional)> // name: type, is_editable, action, <default_value (Optional)>
// } // }
// //
// Where none_action applied when the value wasn't provided and can be: // Where action applied when the value wasn't provided and can be:
// def: Use a default value // def: Use a default value
// auto: Value is auto generated based on other values // auto: Value is auto generated based on other values
// option: Value is optional // option: Value is optional
// gen: Value is always autogenerated and it's original value ignored
make_config! { make_config! {
folders { folders {
/// Data folder |> Main data folder /// Data folder |> Main data folder
@@ -231,12 +244,21 @@ make_config! {
domain: String, true, def, "http://localhost".to_string(); domain: String, true, def, "http://localhost".to_string();
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
domain_set: bool, false, def, false; domain_set: bool, false, def, false;
/// Domain origin |> Domain URL origin (in https://example.com:8443/path, https://example.com:8443 is the origin)
domain_origin: String, false, auto, |c| extract_url_origin(&c.domain);
/// Domain path |> Domain URL path (in https://example.com:8443/path, /path is the path)
domain_path: String, false, auto, |c| extract_url_path(&c.domain);
/// Enable web vault /// Enable web vault
web_vault_enabled: bool, false, def, true; web_vault_enabled: bool, false, def, true;
/// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
hibp_api_key: Pass, true, option; hibp_api_key: Pass, true, option;
/// Per-user attachment limit (KB) |> Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more
user_attachment_limit: i64, true, option;
/// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more
org_attachment_limit: i64, true, option;
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, /// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again. /// otherwise it will delete them and they won't be downloaded again.
@@ -262,10 +284,18 @@ make_config! {
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
admin_token: Pass, true, option; admin_token: Pass, true, option;
/// Invitation organization name |> Name shown in the invitation emails that don't come from a specific organization
invitation_org_name: String, true, def, "Bitwarden_RS".to_string();
}, },
/// Advanced settings /// Advanced settings
advanced { advanced {
/// Client IP header |> If not present, the remote IP is used.
/// Set to the string "none" (without quotes), to disable any headers and just use the remote IP
ip_header: String, true, def, "X-Real-IP".to_string();
/// Internal IP header property, used to avoid recomputing each time
_ip_header_enabled: bool, false, gen, |c| &c.ip_header.trim().to_lowercase() != "none";
/// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded /// Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
icon_cache_ttl: u64, true, def, 2_592_000; icon_cache_ttl: u64, true, def, 2_592_000;
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
@@ -294,9 +324,6 @@ make_config! {
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. /// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
/// ONLY use this during development, as it can slow down the server /// ONLY use this during development, as it can slow down the server
reload_templates: bool, true, def, false; reload_templates: bool, true, def, false;
/// Log routes at launch (Dev)
log_mounts: bool, true, def, false;
/// Enable extended logging /// Enable extended logging
extended_logging: bool, false, def, true; extended_logging: bool, false, def, true;
/// Enable the log to output to Syslog /// Enable the log to output to Syslog
@@ -312,6 +339,9 @@ make_config! {
/// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front /// Bypass admin page security (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
disable_admin_token: bool, true, def, false; disable_admin_token: bool, true, def, false;
/// Allowed iframe ancestors (Know the risks!) |> Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets
allowed_iframe_ancestors: String, true, def, String::new();
}, },
/// Yubikey settings /// Yubikey settings
@@ -381,7 +411,6 @@ make_config! {
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
let db_url = cfg.database_url.to_lowercase(); let db_url = cfg.database_url.to_lowercase();
if cfg!(feature = "sqlite") && (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:")) { if cfg!(feature = "sqlite") && (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:")) {
err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite") err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite")
} }
@@ -394,8 +423,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server") err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server")
} }
let dom = cfg.domain.to_lowercase();
if !dom.starts_with("http://") && !dom.starts_with("https://") {
err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'");
}
if let Some(ref token) = cfg.admin_token { if let Some(ref token) = cfg.admin_token {
if token.trim().is_empty() { if token.trim().is_empty() && !cfg.disable_admin_token {
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`") err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
} }
} }
@@ -436,6 +470,29 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Extracts an RFC 6454 web origin from a URL.
fn extract_url_origin(url: &str) -> String {
match Url::parse(url) {
Ok(u) => u.origin().ascii_serialization(),
Err(e) => {
println!("Error validating domain: {}", e);
String::new()
}
}
}
/// Extracts the path from a URL.
/// All trailing '/' chars are trimmed, even if the path is a lone '/'.
fn extract_url_path(url: &str) -> String {
match Url::parse(url) {
Ok(u) => u.path().trim_end_matches('/').to_string(),
Err(_) => {
// We already print it in the method above, no need to do it again
String::new()
}
}
}
impl Config { impl Config {
pub fn load() -> Result<Self, Error> { pub fn load() -> Result<Self, Error> {
// Loading from env and file // Loading from env and file
@@ -450,12 +507,7 @@ impl Config {
validate_config(&config)?; validate_config(&config)?;
Ok(Config { Ok(Config {
inner: RwLock::new(Inner { inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }),
templates: load_templates(&config.templates_folder),
config,
_env,
_usr,
}),
}) })
} }
@@ -503,10 +555,14 @@ impl Config {
let e: Vec<&str> = email.rsplitn(2, '@').collect(); let e: Vec<&str> = email.rsplitn(2, '@').collect();
if e.len() != 2 || e[0].is_empty() || e[1].is_empty() { if e.len() != 2 || e[0].is_empty() || e[1].is_empty() {
warn!("Failed to parse email address '{}'", email); warn!("Failed to parse email address '{}'", email);
return false return false;
} }
self.signups_domains_whitelist().split(',').any(|d| d == e[0]) // Allow signups if the whitelist is empty/not configured
// (it doesn't contain any domains), or if it matches at least
// one domain.
let whitelist_str = self.signups_domains_whitelist();
( whitelist_str.is_empty() && CONFIG.signups_allowed() )|| whitelist_str.split(',').filter(|s| !s.is_empty()).any(|d| d == e[0])
} }
pub fn delete_user_config(&self) -> Result<(), Error> { pub fn delete_user_config(&self) -> Result<(), Error> {
@@ -568,7 +624,7 @@ impl Config {
) -> Result<String, crate::error::Error> { ) -> Result<String, crate::error::Error> {
if CONFIG.reload_templates() { if CONFIG.reload_templates() {
warn!("RELOADING TEMPLATES"); warn!("RELOADING TEMPLATES");
let hb = load_templates(CONFIG.templates_folder().as_ref()); let hb = load_templates(CONFIG.templates_folder());
hb.render(name, data).map_err(Into::into) hb.render(name, data).map_err(Into::into)
} else { } else {
let hb = &CONFIG.inner.read().unwrap().templates; let hb = &CONFIG.inner.read().unwrap().templates;
@@ -577,17 +633,18 @@ impl Config {
} }
} }
use handlebars::{ use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, Renderable};
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError, Renderable,
};
fn load_templates(path: &str) -> Handlebars { fn load_templates<P>(path: P) -> Handlebars<'static>
where
P: AsRef<std::path::Path>,
{
let mut hb = Handlebars::new(); let mut hb = Handlebars::new();
// Error on missing params // Error on missing params
hb.set_strict_mode(true); hb.set_strict_mode(true);
// Register helpers // Register helpers
hb.register_helper("case", Box::new(CaseHelper)); hb.register_helper("case", Box::new(case_helper));
hb.register_helper("jsesc", Box::new(JsEscapeHelper)); hb.register_helper("jsesc", Box::new(js_escape_helper));
macro_rules! reg { macro_rules! reg {
($name:expr) => {{ ($name:expr) => {{
@@ -613,6 +670,7 @@ fn load_templates(path: &str) -> Handlebars {
reg!("email/verify_email", ".html"); reg!("email/verify_email", ".html");
reg!("email/welcome", ".html"); reg!("email/welcome", ".html");
reg!("email/welcome_must_verify", ".html"); reg!("email/welcome_must_verify", ".html");
reg!("email/smtp_test", ".html");
reg!("admin/base"); reg!("admin/base");
reg!("admin/login"); reg!("admin/login");
@@ -626,15 +684,11 @@ fn load_templates(path: &str) -> Handlebars {
hb hb
} }
pub struct CaseHelper; fn case_helper<'reg, 'rc>(
impl HelperDef for CaseHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>, h: &Helper<'reg, 'rc>,
r: &'reg Handlebars, r: &'reg Handlebars,
ctx: &Context, ctx: &'rc Context,
rc: &mut RenderContext<'reg>, rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output, out: &mut dyn Output,
) -> HelperResult { ) -> HelperResult {
let param = h let param = h
@@ -648,17 +702,12 @@ impl HelperDef for CaseHelper {
Ok(()) Ok(())
} }
} }
}
pub struct JsEscapeHelper; fn js_escape_helper<'reg, 'rc>(
impl HelperDef for JsEscapeHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>, h: &Helper<'reg, 'rc>,
_: &'reg Handlebars, _r: &'reg Handlebars,
_: &Context, _ctx: &'rc Context,
_: &mut RenderContext<'reg>, _rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output, out: &mut dyn Output,
) -> HelperResult { ) -> HelperResult {
let param = h let param = h
@@ -676,4 +725,3 @@ impl HelperDef for JsEscapeHelper {
out.write(&quoted_value)?; out.write(&quoted_value)?;
Ok(()) Ok(())
} }
}

View File

@@ -2,9 +2,9 @@
// PBKDF2 derivation // PBKDF2 derivation
// //
use crate::error::Error;
use ring::{digest, hmac, pbkdf2}; use ring::{digest, hmac, pbkdf2};
use std::num::NonZeroU32; use std::num::NonZeroU32;
use crate::error::Error;
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256; static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN; const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;

View File

@@ -49,7 +49,7 @@ impl Attachment {
} }
} }
use crate::db::schema::attachments; use crate::db::schema::{attachments, ciphers};
use crate::db::DbConn; use crate::db::DbConn;
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
@@ -118,4 +118,26 @@ impl Attachment {
.load::<Self>(&**conn) .load::<Self>(&**conn)
.expect("Error loading attachments") .expect("Error loading attachments")
} }
pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
let result: Option<i64> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.filter(ciphers::user_uuid.eq(user_uuid))
.select(diesel::dsl::sum(attachments::file_size))
.first(&**conn)
.expect("Error loading user attachment total size");
result.unwrap_or(0)
}
pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
let result: Option<i64> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.filter(ciphers::organization_uuid.eq(org_uuid))
.select(diesel::dsl::sum(attachments::file_size))
.first(&**conn)
.expect("Error loading user attachment total size");
result.unwrap_or(0)
}
} }

View File

@@ -73,6 +73,13 @@ impl TwoFactor {
impl TwoFactor { impl TwoFactor {
#[cfg(feature = "postgresql")] #[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))
.execute(&**conn)
.map_res("Error deleting twofactor for insert")?;
diesel::insert_into(twofactor::table) diesel::insert_into(twofactor::table)
.values(self) .values(self)
.on_conflict(twofactor::uuid) .on_conflict(twofactor::uuid)

View File

@@ -319,8 +319,7 @@ impl Invitation {
} }
pub fn take(mail: &str, conn: &DbConn) -> bool { pub fn take(mail: &str, conn: &DbConn) -> bool {
CONFIG.invitations_allowed() match Self::find_by_mail(mail, &conn) {
&& match Self::find_by_mail(mail, &conn) {
Some(invitation) => invitation.delete(&conn).is_ok(), Some(invitation) => invitation.delete(&conn).is_ok(),
None => false, None => false,
} }

View File

@@ -46,6 +46,7 @@ use std::option::NoneError as NoneErr;
use std::time::SystemTimeError as TimeErr; use std::time::SystemTimeError as TimeErr;
use u2f::u2ferror::U2fError as U2fErr; use u2f::u2ferror::U2fError as U2fErr;
use yubico::yubicoerror::YubicoError as YubiErr; use yubico::yubicoerror::YubicoError as YubiErr;
use lettre::smtp::error::Error as LettreErr;
#[derive(Display, Serialize)] #[derive(Display, Serialize)]
pub struct Empty {} pub struct Empty {}
@@ -73,6 +74,7 @@ make_error! {
ReqError(ReqErr): _has_source, _api_error, ReqError(ReqErr): _has_source, _api_error,
RegexError(RegexErr): _has_source, _api_error, RegexError(RegexErr): _has_source, _api_error,
YubiError(YubiErr): _has_source, _api_error, YubiError(YubiErr): _has_source, _api_error,
LetreErr(LettreErr): _has_source, _api_error,
} }
// This is implemented by hand because NoneError doesn't implement neither Display nor Error // This is implemented by hand because NoneError doesn't implement neither Display nor Error
@@ -86,7 +88,18 @@ impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.source() { match self.source() {
Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e), Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e),
None => write!(f, "{}. {}", self.message, self.error), None => match self.error {
ErrorKind::EmptyError(_) => Ok(()),
ErrorKind::SimpleError(ref s) => {
if &self.message == s {
write!(f, "{}", self.message)
} else {
write!(f, "{}. {}", self.message, s)
}
}
ErrorKind::JsonError(_) => write!(f, "{}", self.message),
_ => unreachable!(),
},
} }
} }
} }
@@ -170,15 +183,17 @@ use rocket::response::{self, Responder, Response};
impl<'r> Responder<'r> for Error { impl<'r> Responder<'r> for Error {
fn respond_to(self, _: &Request) -> response::Result<'r> { fn respond_to(self, _: &Request) -> response::Result<'r> {
let usr_msg = format!("{}", self); match self.error {
error!("{:#?}", self); ErrorKind::EmptyError(_) => {} // Don't print the error in this situation
_ => error!(target: "error", "{:#?}", self),
};
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest); let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
Response::build() Response::build()
.status(code) .status(code)
.header(ContentType::JSON) .header(ContentType::JSON)
.sized_body(Cursor::new(usr_msg)) .sized_body(Cursor::new(format!("{}", self)))
.ok() .ok()
} }
} }
@@ -196,21 +211,33 @@ macro_rules! err {
}}; }};
} }
#[macro_export]
macro_rules! err_discard {
($msg:expr, $data:expr) => {{
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
return Err(crate::error::Error::new($msg, $msg));
}};
($usr_msg:expr, $log_value:expr, $data:expr) => {{
std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();
return Err(crate::error::Error::new($usr_msg, $log_value));
}};
}
#[macro_export] #[macro_export]
macro_rules! err_json { macro_rules! err_json {
($expr:expr) => {{ ($expr:expr, $log_value:expr) => {{
return Err(crate::error::Error::from($expr)); return Err(($log_value, $expr).into());
}}; }};
} }
#[macro_export] #[macro_export]
macro_rules! err_handler { macro_rules! err_handler {
($expr:expr) => {{ ($expr:expr) => {{
error!("Unauthorized Error: {}", $expr); error!(target: "auth", "Unauthorized Error: {}", $expr);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr)); return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
}}; }};
($usr_msg:expr, $log_value:expr) => {{ ($usr_msg:expr, $log_value:expr) => {{
error!("Unauthorized Error: {}. {}", $usr_msg, $log_value); error!(target: "auth", "Unauthorized Error: {}. {}", $usr_msg, $log_value);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg)); return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $usr_msg));
}}; }};
} }

View File

@@ -1,14 +1,16 @@
use lettre::smtp::authentication::Credentials; use lettre::smtp::authentication::Credentials;
use lettre::smtp::authentication::Mechanism as SmtpAuthMechanism; use lettre::smtp::authentication::Mechanism as SmtpAuthMechanism;
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport}; use lettre::{
use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder}; builder::{EmailBuilder, MimeMultipartType, PartBuilder},
ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport,
};
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use quoted_printable::encode_to_str; use quoted_printable::encode_to_str;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::auth::{encode_jwt, generate_invite_claims, generate_delete_claims, generate_verify_email_claims}; use crate::auth::{encode_jwt, generate_delete_claims, generate_invite_claims, generate_verify_email_claims};
use crate::error::Error; use crate::error::Error;
use crate::CONFIG; use crate::CONFIG;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@@ -96,9 +98,7 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
} }
pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_delete_claims( let claims = generate_delete_claims(uuid.to_string());
uuid.to_string(),
);
let delete_token = encode_jwt(&claims); let delete_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
@@ -115,9 +115,7 @@ pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
} }
pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_verify_email_claims( let claims = generate_verify_email_claims(uuid.to_string());
uuid.to_string(),
);
let verify_email_token = encode_jwt(&claims); let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
@@ -145,9 +143,7 @@ pub fn send_welcome(address: &str) -> EmptyResult {
} }
pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_verify_email_claims( let claims = generate_verify_email_claims(uuid.to_string());
uuid.to_string(),
);
let verify_email_token = encode_jwt(&claims); let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text( let (subject, body_html, body_text) = get_text(
@@ -262,7 +258,30 @@ 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 {
let (subject, body_html, body_text) = get_text(
"email/smtp_test",
json!({
"url": CONFIG.domain(),
}),
)?;
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: &str, body_text: &str) -> EmptyResult {
let address_split: Vec<&str> = address.rsplitn(2, '@').collect();
if address_split.len() != 2 {
err!("Invalid email address (no @)");
}
let domain_puny = match idna::domain_to_ascii_strict(address_split[0]) {
Ok(d) => d,
Err(_) => err!("Can't convert email domain to ASCII representation"),
};
let address = format!("{}@{}", address_split[1], domain_puny);
let html = PartBuilder::new() let html = PartBuilder::new()
.body(encode_to_str(body_html)) .body(encode_to_str(body_html))
.header(("Content-Type", "text/html; charset=utf-8")) .header(("Content-Type", "text/html; charset=utf-8"))
@@ -290,12 +309,11 @@ fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) ->
let mut transport = mailer(); let mut transport = mailer();
let result = transport let result = transport.send(email);
.send(email.into())
.map_err(|e| Error::new("Error sending email", e.to_string()))
.and(Ok(()));
// Explicitly close the connection, in case of error // Explicitly close the connection, in case of error
transport.close(); transport.close();
result
result?;
Ok(())
} }

View File

@@ -16,16 +16,15 @@ extern crate diesel;
#[macro_use] #[macro_use]
extern crate diesel_migrations; extern crate diesel_migrations;
#[macro_use] #[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate derive_more; extern crate derive_more;
#[macro_use] #[macro_use]
extern crate num_derive; extern crate num_derive;
use std::{ use std::{
fs::create_dir_all,
path::Path, path::Path,
process::{exit, Command}, process::{exit, Command},
fs::create_dir_all, str::FromStr,
}; };
#[macro_use] #[macro_use]
@@ -41,12 +40,28 @@ mod util;
pub use config::CONFIG; pub use config::CONFIG;
pub use error::{Error, MapResult}; pub use error::{Error, MapResult};
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(name = "bitwarden_rs", about = "A Bitwarden API server written in Rust")]
struct Opt {
/// Prints the app version
#[structopt(short, long)]
version: bool,
}
fn main() { fn main() {
parse_args();
launch_info(); launch_info();
if CONFIG.extended_logging() { use log::LevelFilter as LF;
init_logging().ok(); 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,
};
check_db(); check_db();
check_rsa_keys(); check_rsa_keys();
@@ -55,7 +70,19 @@ fn main() {
create_icon_cache_folder(); create_icon_cache_folder();
launch_rocket(); launch_rocket(extra_debug);
}
fn parse_args() {
let opt = Opt::from_args();
if opt.version {
if let Some(version) = option_env!("GIT_VERSION") {
println!("bitwarden_rs {}", version);
} else {
println!("bitwarden_rs (Version info from Git not present)");
}
exit(0);
}
} }
fn launch_info() { fn launch_info() {
@@ -73,10 +100,23 @@ fn launch_info() {
println!("\\--------------------------------------------------------------------/\n"); println!("\\--------------------------------------------------------------------/\n");
} }
fn init_logging() -> Result<(), fern::InitError> { fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
use std::str::FromStr;
let mut logger = fern::Dispatch::new() let mut logger = fern::Dispatch::new()
.format(|out, message, record| { .level(level)
// Hide unknown certificate errors if using self-signed
.level_for("rustls::session", log::LevelFilter::Off)
// Hide failed to close stream messages
.level_for("hyper::server", log::LevelFilter::Warn)
// Silence rocket logs
.level_for("_", log::LevelFilter::Off)
.level_for("launch", log::LevelFilter::Off)
.level_for("launch_", log::LevelFilter::Off)
.level_for("rocket::rocket", log::LevelFilter::Off)
.level_for("rocket::fairing", log::LevelFilter::Off)
.chain(std::io::stdout());
if CONFIG.extended_logging() {
logger = logger.format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
"{}[{}][{}] {}", "{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"), chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
@@ -84,13 +124,10 @@ fn init_logging() -> Result<(), fern::InitError> {
record.level(), record.level(),
message message
)) ))
}) });
.level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level")) } else {
// Hide unknown certificate errors if using self-signed logger = logger.format(|out, message, _| out.finish(format_args!("{}", message)));
.level_for("rustls::session", log::LevelFilter::Off) }
// Hide failed to close stream messages
.level_for("hyper::server", log::LevelFilter::Warn)
.chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file() { if let Some(log_file) = CONFIG.log_file() {
logger = logger.chain(fern::log_file(log_file)?); logger = logger.chain(fern::log_file(log_file)?);
@@ -141,7 +178,7 @@ fn check_db() {
// Turn on WAL in SQLite // Turn on WAL in SQLite
if CONFIG.enable_db_wal() { if CONFIG.enable_db_wal() {
use diesel::RunQueryDsl; use diesel::RunQueryDsl;
let connection = db::get_connection().expect("Can't conect to DB"); let connection = db::get_connection().expect("Can't connect to DB");
diesel::sql_query("PRAGMA journal_mode=wal") diesel::sql_query("PRAGMA journal_mode=wal")
.execute(&connection) .execute(&connection)
.expect("Failed to turn on WAL"); .expect("Failed to turn on WAL");
@@ -161,7 +198,9 @@ fn check_rsa_keys() {
info!("JWT keys don't exist, checking if OpenSSL is available..."); info!("JWT keys don't exist, checking if OpenSSL is available...");
Command::new("openssl").arg("version").status().unwrap_or_else(|_| { Command::new("openssl").arg("version").status().unwrap_or_else(|_| {
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH"); info!(
"Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH"
);
exit(1); exit(1);
}); });
@@ -209,7 +248,9 @@ fn check_web_vault() {
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html"); let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
if !index_path.exists() { if !index_path.exists() {
error!("Web vault is not found. To install it, please follow the steps in https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault"); error!("Web vault is not found. To install it, please follow the steps in: ");
error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
exit(1); exit(1);
} }
} }
@@ -236,33 +277,26 @@ mod migrations {
} }
} }
fn launch_rocket() { fn launch_rocket(extra_debug: bool) {
// Create Rocket object, this stores current log level and sets it's own // Create Rocket object, this stores current log level and sets its own
let rocket = rocket::ignite(); let rocket = rocket::ignite();
// If we aren't logging the mounts, we force the logging level down let basepath = &CONFIG.domain_path();
if !CONFIG.log_mounts() {
log::set_max_level(log::LevelFilter::Warn);
}
let rocket = rocket
.mount("/", api::web_routes())
.mount("/api", api::core_routes())
.mount("/admin", api::admin_routes())
.mount("/identity", api::identity_routes())
.mount("/icons", api::icons_routes())
.mount("/notifications", api::notifications_routes());
// Force the level up for the fairings, managed state and lauch
if !CONFIG.log_mounts() {
log::set_max_level(log::LevelFilter::max());
}
// If adding more paths here, consider also adding them to
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
let rocket = rocket let rocket = rocket
.mount(&[basepath, "/"].concat(), api::web_routes())
.mount(&[basepath, "/api"].concat(), api::core_routes())
.mount(&[basepath, "/admin"].concat(), api::admin_routes())
.mount(&[basepath, "/identity"].concat(), api::identity_routes())
.mount(&[basepath, "/icons"].concat(), api::icons_routes())
.mount(&[basepath, "/notifications"].concat(), api::notifications_routes())
.manage(db::init_pool()) .manage(db::init_pool())
.manage(api::start_notification_server()) .manage(api::start_notification_server())
.attach(util::AppHeaders()) .attach(util::AppHeaders())
.attach(util::CORS()); .attach(util::CORS())
.attach(util::BetterLogging(extra_debug));
// Launch and print error if there is one // Launch and print error if there is one
// The launch will restore the original logging level // The launch will restore the original logging level

View File

@@ -6,10 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Bitwarden_rs Admin Panel</title> <title>Bitwarden_rs Admin Panel</title>
<link rel="stylesheet" href="/bwrs_static/bootstrap.css" /> <link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" />
<script src="/bwrs_static/bootstrap-native-v4.js"></script> <script src="{{urlpath}}/bwrs_static/bootstrap-native-v4.js"></script>
<script src="/bwrs_static/md5.js"></script> <script src="{{urlpath}}/bwrs_static/md5.js"></script>
<script src="/bwrs_static/identicon.js"></script> <script src="{{urlpath}}/bwrs_static/identicon.js"></script>
<style> <style>
body { body {
padding-top: 70px; padding-top: 70px;
@@ -33,21 +33,32 @@
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top shadow"> <nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top shadow">
<a class="navbar-brand" href="#">Bitwarden_rs</a> <a class="navbar-brand" href="#">Bitwarden_rs</a>
<div class="navbar-collapse"> <div class="navbar-collapse">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="/admin">Admin Panel</a> <a class="nav-link" href="{{urlpath}}/admin">Admin Panel</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Vault</a> <a class="nav-link" href="{{urlpath}}/">Vault</a>
</li> </li>
</ul> </ul>
</div> </div>
<ul class="navbar-nav">
{{#if version}} {{#if version}}
<div class="navbar-text">Version: {{version}}</div> <li class="nav-item">
<span class="navbar-text mr-2">Version: {{version}}</span>
</li>
{{/if}} {{/if}}
{{#if logged_in}}
<li class="nav-item">
<a class="nav-link" href="{{urlpath}}/admin/logout">Log Out</a>
</li>
{{/if}}
</ul>
</nav> </nav>
{{> (page_content) }} {{> (page_content) }}

View File

@@ -14,7 +14,7 @@
<form class="form-inline" method="post"> <form class="form-inline" method="post">
<input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token"> <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Enter</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -71,6 +71,7 @@
them to avoid confusion. This does not apply to the read-only section, which can only be set through the them to avoid confusion. This does not apply to the read-only section, which can only be set through the
environment. environment.
</div> </div>
<form class="form accordion" id="config-form" onsubmit="saveConfig(); return false;"> <form class="form accordion" id="config-form" onsubmit="saveConfig(); return false;">
{{#each config}} {{#each config}}
{{#if groupdoc}} {{#if groupdoc}}
@@ -110,6 +111,17 @@
</div> </div>
{{/if}} {{/if}}
{{/each}} {{/each}}
{{#case group "smtp"}}
<div class="form-group row pt-3 border-top" title="Send a test email to given email address">
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
<div class="col-sm-8 input-group">
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email">
<div class="input-group-append">
<button type="button" class="btn btn-outline-primary" onclick="smtpTest(); return false;">Send test email</button>
</div>
</div>
</div>
{{/case}}
</div> </div>
</div> </div>
{{/if}} {{/if}}
@@ -191,7 +203,10 @@
<script> <script>
function reload() { window.location.reload(); } function reload() { window.location.reload(); }
function msg(text) { alert(text); reload(); } function msg(text, reload_page = true) {
text && alert(text);
reload_page && reload();
}
function identicon(email) { function identicon(email) {
const data = new Identicon(md5(email), { size: 48, format: 'svg' }); const data = new Identicon(md5(email), { size: 48, format: 'svg' });
return "data:image/svg+xml;base64," + data.toString(); return "data:image/svg+xml;base64," + data.toString();
@@ -206,26 +221,37 @@
} }
return false; return false;
} }
function _post(url, successMsg, errMsg, body) { function _post(url, successMsg, errMsg, body, reload_page = true) {
fetch(url, { fetch(url, {
method: 'POST', method: 'POST',
body: body, body: body,
mode: "same-origin", mode: "same-origin",
credentials: "same-origin", credentials: "same-origin",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}).then(e => { }).then( resp => {
if (e.ok) { return msg(successMsg); } if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
e.json().then(json => { respStatus = resp.status;
const msg = json ? json.ErrorModel.Message : "Unknown error"; respStatusText = resp.statusText;
msg(errMsg + ": " + msg); return resp.text();
}).then( respText => {
try {
const respJson = JSON.parse(respText);
return respJson ? respJson.ErrorModel.Message : "Unknown error";
} catch (e) {
return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
}
}).then( apiMsg => {
msg(errMsg + "\n" + apiMsg, reload_page);
}).catch( e => {
if (e.error === false) { return true; }
else { msg(errMsg + "\n" + e.body, reload_page); }
}); });
}).catch(e => { msg(errMsg + ": Unknown error") });
} }
function deleteUser(id, mail) { function deleteUser(id, mail) {
var input_mail = prompt("To delete user '" + mail + "', please type the email below") var input_mail = prompt("To delete user '" + mail + "', please type the email below")
if (input_mail != null) { if (input_mail != null) {
if (input_mail == mail) { if (input_mail == mail) {
_post("/admin/users/" + id + "/delete", _post("{{urlpath}}/admin/users/" + id + "/delete",
"User deleted correctly", "User deleted correctly",
"Error deleting user"); "Error deleting user");
} else { } else {
@@ -235,19 +261,19 @@
return false; return false;
} }
function remove2fa(id) { function remove2fa(id) {
_post("/admin/users/" + id + "/remove-2fa", _post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
"2FA removed correctly", "2FA removed correctly",
"Error removing 2FA"); "Error removing 2FA");
return false; return false;
} }
function deauthUser(id) { function deauthUser(id) {
_post("/admin/users/" + id + "/deauth", _post("{{urlpath}}/admin/users/" + id + "/deauth",
"Sessions deauthorized correctly", "Sessions deauthorized correctly",
"Error deauthorizing sessions"); "Error deauthorizing sessions");
return false; return false;
} }
function updateRevisions() { function updateRevisions() {
_post("/admin/users/update_revision", _post("{{urlpath}}/admin/users/update_revision",
"Success, clients will sync next time they connect", "Success, clients will sync next time they connect",
"Error forcing clients to sync"); "Error forcing clients to sync");
return false; return false;
@@ -256,10 +282,18 @@
inv = document.getElementById("email-invite"); inv = document.getElementById("email-invite");
data = JSON.stringify({ "email": inv.value }); data = JSON.stringify({ "email": inv.value });
inv.value = ""; inv.value = "";
_post("/admin/invite/", "User invited correctly", _post("{{urlpath}}/admin/invite/", "User invited correctly",
"Error inviting user", data); "Error inviting user", data);
return false; return false;
} }
function smtpTest() {
test_email = document.getElementById("smtp-test-email");
data = JSON.stringify({ "email": test_email.value });
_post("{{urlpath}}/admin/test/smtp/",
"SMTP Test email sent correctly",
"Error sending SMTP test email", data, false);
return false;
}
function getFormData() { function getFormData() {
let data = {}; let data = {};
@@ -278,7 +312,7 @@
} }
function saveConfig() { function saveConfig() {
data = JSON.stringify(getFormData()); data = JSON.stringify(getFormData());
_post("/admin/config/", "Config saved correctly", _post("{{urlpath}}/admin/config/", "Config saved correctly",
"Error saving config", data); "Error saving config", data);
return false; return false;
} }
@@ -286,7 +320,7 @@
var input = prompt("This will remove all user configurations, and restore the defaults and the " + var input = prompt("This will remove all user configurations, and restore the defaults and the " +
"values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:"); "values set by the environment. This operation could be dangerous. Type 'DELETE' to proceed:");
if (input === "DELETE") { if (input === "DELETE") {
_post("/admin/config/delete", _post("{{urlpath}}/admin/config/delete",
"Config deleted correctly", "Config deleted correctly",
"Error deleting config"); "Error deleting config");
} else { } else {
@@ -296,9 +330,9 @@
return false; return false;
} }
function backupDatabase() { function backupDatabase() {
_post("/admin/config/backup_db", _post("{{urlpath}}/admin/config/backup_db",
"Backup created successfully", "Backup created successfully",
"Error creating backup"); "Error creating backup", null, false);
return false; return false;
} }
function masterCheck(check_id, inputs_query) { function masterCheck(check_id, inputs_query) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
Bitwarden_rs SMTP Test
<!---------------->
<html>
<p>
This is a test email to verify the SMTP configuration for <a href="{{url}}">{{url}}</a>.
</p>
<p>When you can read this email it is probably configured correctly.</p>
</html>

View File

@@ -0,0 +1,129 @@
Bitwarden_rs SMTP Test
<!---------------->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Bitwarden_rs</title>
</head>
<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
<style type="text/css">
 body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
color: #333;
line-height: 25px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
}
body * {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
color: #333;
line-height: 25px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
}
img {
max-width: 100%;
border: none;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 25px;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 600px) {
body {
padding: 0 !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.container-table {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 0 10px 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
.main {
border-right: none !important;
border-left: none !important;
border-radius: 0 !important;
}
.logo {
padding-top: 10px !important;
}
.footer {
margin-top: 10px !important;
}
.indented {
padding-left: 10px;
}
}
</style>
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
<img src="{{url}}/bwrs_static/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
</td>
</tr>
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
<table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
This is a test email to verify the SMTP configuration for <a href="{{url}}">{{url}}</a>.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
When you can read this email it is probably configured correctly.
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_static/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,13 @@
// Web Headers and caching // Web Headers and caching
// //
use rocket::fairing::{Fairing, Info, Kind}; use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{ContentType, Header, HeaderMap, Method, Status};
use rocket::response::{self, Responder}; use rocket::response::{self, Responder};
use rocket::{Request, Response}; use rocket::{Data, Request, Response, Rocket};
use rocket::http::{Header, HeaderMap, ContentType, Method, Status};
use std::io::Cursor; use std::io::Cursor;
use crate::CONFIG;
pub struct AppHeaders(); pub struct AppHeaders();
impl Fairing for AppHeaders { impl Fairing for AppHeaders {
@@ -23,7 +25,7 @@ impl Fairing for AppHeaders {
res.set_raw_header("X-Frame-Options", "SAMEORIGIN"); res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
res.set_raw_header("X-Content-Type-Options", "nosniff"); res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-XSS-Protection", "1; mode=block"); res.set_raw_header("X-XSS-Protection", "1; mode=block");
let csp = "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://*;"; let csp = format!("frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://* {};", CONFIG.allowed_iframe_ancestors());
res.set_raw_header("Content-Security-Policy", csp); res.set_raw_header("Content-Security-Policy", csp);
// Disable cache unless otherwise specified // Disable cache unless otherwise specified
@@ -55,7 +57,7 @@ impl Fairing for CORS {
fn info(&self) -> Info { fn info(&self) -> Info {
Info { Info {
name: "CORS", name: "CORS",
kind: Kind::Response kind: Kind::Response,
} }
} }
@@ -107,6 +109,83 @@ impl<'r, R: Responder<'r>> Responder<'r> for Cached<R> {
} }
} }
// Log all the routes from the main paths list, and the attachments endpoint
// Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 6] = [
"/api",
"/admin",
"/identity",
"/icons",
"/notifications/hub/negotiate",
"/attachments",
];
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
pub struct BetterLogging(pub bool);
impl Fairing for BetterLogging {
fn info(&self) -> Info {
Info {
name: "Better Logging",
kind: Kind::Launch | Kind::Request | Kind::Response,
}
}
fn on_launch(&self, rocket: &Rocket) {
if self.0 {
info!(target: "routes", "Routes loaded:");
let mut routes: Vec<_> = rocket.routes().collect();
routes.sort_by_key(|r| r.uri.path());
for route in routes {
if route.rank < 0 {
info!(target: "routes", "{:<6} {}", route.method, route.uri);
} else {
info!(target: "routes", "{:<6} {} [{}]", route.method, route.uri, route.rank);
}
}
}
let config = rocket.config();
let scheme = if config.tls_enabled() { "https" } else { "http" };
let addr = format!("{}://{}:{}", &scheme, &config.address, &config.port);
info!(target: "start", "Rocket has launched from {}", addr);
}
fn on_request(&self, request: &mut Request<'_>, _data: &Data) {
let method = request.method();
if !self.0 && method == Method::Options {
return;
}
let uri = request.uri();
let uri_path = uri.path();
// FIXME: trim_start_matches() could result in over-trimming in pathological cases;
// strip_prefix() would be a better option once it's stable.
let uri_subpath = uri_path.trim_start_matches(&CONFIG.domain_path());
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
match uri.query() {
Some(q) => info!(target: "request", "{} {}?{}", method, uri_path, &q[..q.len().min(30)]),
None => info!(target: "request", "{} {}", method, uri_path),
};
}
}
fn on_response(&self, request: &Request, response: &mut Response) {
if !self.0 && request.method() == Method::Options {
return;
}
// FIXME: trim_start_matches() could result in over-trimming in pathological cases;
// strip_prefix() would be a better option once it's stable.
let uri_subpath = request.uri().path().trim_start_matches(&CONFIG.domain_path());
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
let status = response.status();
if let Some(ref route) = request.route() {
info!(target: "response", "{} => {} {}", route, status.code, status.reason)
} else {
info!(target: "response", "{} {}", status.code, status.reason)
}
}
}
}
// //
// File handling // File handling
// //
@@ -148,6 +227,33 @@ pub fn delete_file(path: &str) -> IOResult<()> {
res res
} }
pub struct LimitedReader<'a> {
reader: &'a mut dyn std::io::Read,
limit: usize, // In bytes
count: usize,
}
impl<'a> LimitedReader<'a> {
pub fn new(reader: &'a mut dyn std::io::Read, limit: usize) -> LimitedReader<'a> {
LimitedReader {
reader,
limit,
count: 0,
}
}
}
impl<'a> std::io::Read for LimitedReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.count += buf.len();
if self.count > self.limit {
Ok(0) // End of the read
} else {
self.reader.read(buf)
}
}
}
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"]; const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
pub fn get_display_size(size: i32) -> String { pub fn get_display_size(size: i32) -> String {
@@ -212,6 +318,17 @@ where
try_parse_string(env::var(key)) try_parse_string(env::var(key))
} }
const TRUE_VALUES: &[&str] = &["true", "t", "yes", "y", "1"];
const FALSE_VALUES: &[&str] = &["false", "f", "no", "n", "0"];
pub fn get_env_bool(key: &str) -> Option<bool> {
match env::var(key) {
Ok(val) if TRUE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(true),
Ok(val) if FALSE_VALUES.contains(&val.to_lowercase().as_ref()) => Some(false),
_ => None,
}
}
// //
// Date util methods // Date util methods
// //