mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 18:55:57 +03:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b557c11724 | ||
|
a1204cc935 | ||
|
1ea511cbfc | ||
|
2e6a6fa39f | ||
|
e7d5c17ff7 | ||
|
a7be8fab9b | ||
|
39d4d31080 | ||
|
c28246cf34 | ||
|
d7df0ad79e | ||
|
7c8ba0c232 | ||
|
d335187172 | ||
|
f858523d92 | ||
|
529c39c6c5 | ||
|
b428481ac0 | ||
|
b4b2701905 | ||
|
de66e56b6c | ||
|
ecfebaf3c7 | ||
|
0e53f58288 | ||
|
bc7ceb2ee3 | ||
|
b27e6e30c9 | ||
|
505b30eec2 | ||
|
54bfcb8bc3 | ||
|
035f694d2f | ||
|
a4ab014ade | ||
|
6fedfceaa9 | ||
|
8e8483481f |
@@ -92,15 +92,20 @@
|
|||||||
##########################
|
##########################
|
||||||
|
|
||||||
## Enables push notifications (requires key and id from https://bitwarden.com/host)
|
## Enables push notifications (requires key and id from https://bitwarden.com/host)
|
||||||
## If you choose "European Union" Data Region, uncomment PUSH_RELAY_URI and PUSH_IDENTITY_URI then replace .com by .eu
|
|
||||||
## Details about mobile client push notification:
|
## Details about mobile client push notification:
|
||||||
## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification
|
## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification
|
||||||
# PUSH_ENABLED=false
|
# PUSH_ENABLED=false
|
||||||
# PUSH_INSTALLATION_ID=CHANGEME
|
# PUSH_INSTALLATION_ID=CHANGEME
|
||||||
# PUSH_INSTALLATION_KEY=CHANGEME
|
# PUSH_INSTALLATION_KEY=CHANGEME
|
||||||
## Don't change this unless you know what you're doing.
|
|
||||||
|
# WARNING: Do not modify the following settings unless you fully understand their implications!
|
||||||
|
# Default Push Relay and Identity URIs
|
||||||
# PUSH_RELAY_URI=https://push.bitwarden.com
|
# PUSH_RELAY_URI=https://push.bitwarden.com
|
||||||
# PUSH_IDENTITY_URI=https://identity.bitwarden.com
|
# PUSH_IDENTITY_URI=https://identity.bitwarden.com
|
||||||
|
# European Union Data Region Settings
|
||||||
|
# If you have selected "European Union" as your data region, use the following URIs instead.
|
||||||
|
# PUSH_RELAY_URI=https://api.bitwarden.eu
|
||||||
|
# PUSH_IDENTITY_URI=https://identity.bitwarden.eu
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
### Schedule jobs ###
|
### Schedule jobs ###
|
||||||
@@ -152,6 +157,10 @@
|
|||||||
## Cron schedule of the job that cleans old auth requests from the auth request.
|
## Cron schedule of the job that cleans old auth requests from the auth request.
|
||||||
## Defaults to every minute. Set blank to disable this job.
|
## Defaults to every minute. Set blank to disable this job.
|
||||||
# AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *"
|
# AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *"
|
||||||
|
##
|
||||||
|
## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt.
|
||||||
|
## Defaults to every minute. Set blank to disable this job.
|
||||||
|
# DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *"
|
||||||
|
|
||||||
########################
|
########################
|
||||||
### General settings ###
|
### General settings ###
|
||||||
@@ -320,15 +329,15 @@
|
|||||||
## The default is 10 seconds, but this could be to low on slower network connections
|
## The default is 10 seconds, but this could be to low on slower network connections
|
||||||
# ICON_DOWNLOAD_TIMEOUT=10
|
# ICON_DOWNLOAD_TIMEOUT=10
|
||||||
|
|
||||||
## Icon blacklist Regex
|
## Block HTTP domains/IPs by Regex
|
||||||
## Any domains or IPs that match this regex won't be fetched by the icon service.
|
## Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
||||||
## Useful to hide other servers in the local network. Check the WIKI for more details
|
## Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
## NOTE: Always enclose this regex withing single quotes!
|
## NOTE: Always enclose this regex withing single quotes!
|
||||||
# ICON_BLACKLIST_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
# HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$'
|
||||||
|
|
||||||
## Any IP which is not defined as a global IP will be blacklisted.
|
## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address.
|
||||||
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
# ICON_BLACKLIST_NON_GLOBAL_IPS=true
|
# HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true
|
||||||
|
|
||||||
## Client Settings
|
## Client Settings
|
||||||
## Enable experimental feature flags for clients.
|
## Enable experimental feature flags for clients.
|
||||||
@@ -362,8 +371,9 @@
|
|||||||
## 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"
|
||||||
## Setting it to "trace" or "debug" would also show logs for mounted
|
## Setting it to "trace" or "debug" would also show logs for mounted routes and static file, websocket and alive requests
|
||||||
## routes and static file, websocket and alive requests
|
## For a specific module append a comma separated `path::to::module=log_level`
|
||||||
|
## For example, to only see debug logs for icons use: LOG_LEVEL="info,vaultwarden::api::icons=debug"
|
||||||
# LOG_LEVEL=info
|
# LOG_LEVEL=info
|
||||||
|
|
||||||
## Token for the admin interface, preferably an Argon2 PCH string
|
## Token for the admin interface, preferably an Argon2 PCH string
|
||||||
@@ -409,6 +419,12 @@
|
|||||||
## KNOW WHAT YOU ARE DOING!
|
## KNOW WHAT YOU ARE DOING!
|
||||||
# ORG_GROUPS_ENABLED=false
|
# ORG_GROUPS_ENABLED=false
|
||||||
|
|
||||||
|
## Increase secure note size limit (Know the risks!)
|
||||||
|
## Sets the secure note size limit to 100_000 instead of the default 10_000.
|
||||||
|
## WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers!
|
||||||
|
## KNOW WHAT YOU ARE DOING!
|
||||||
|
# INCREASE_NOTE_SIZE_LIMIT=false
|
||||||
|
|
||||||
########################
|
########################
|
||||||
### MFA/2FA settings ###
|
### MFA/2FA settings ###
|
||||||
########################
|
########################
|
||||||
@@ -422,15 +438,21 @@
|
|||||||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
||||||
|
|
||||||
## Duo Settings
|
## Duo Settings
|
||||||
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
|
## You need to configure the DUO_IKEY, DUO_SKEY, and DUO_HOST options to enable global Duo support.
|
||||||
|
## Otherwise users will need to configure it themselves.
|
||||||
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
||||||
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
||||||
## Then set the following options, based on the values obtained from the last step:
|
## Then set the following options, based on the values obtained from the last step:
|
||||||
# DUO_IKEY=<Integration Key>
|
# DUO_IKEY=<Client ID>
|
||||||
# DUO_SKEY=<Secret Key>
|
# DUO_SKEY=<Client Secret>
|
||||||
# DUO_HOST=<API Hostname>
|
# DUO_HOST=<API Hostname>
|
||||||
## After that, you should be able to follow the rest of the guide linked above,
|
## After that, you should be able to follow the rest of the guide linked above,
|
||||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||||
|
##
|
||||||
|
## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'.
|
||||||
|
## Duo no longer supports this, but it still works for some integrations.
|
||||||
|
## If you aren't sure, leave this alone.
|
||||||
|
# DUO_USE_IFRAME=false
|
||||||
|
|
||||||
## Email 2FA settings
|
## Email 2FA settings
|
||||||
## Email token size
|
## Email token size
|
||||||
|
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -63,13 +63,13 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Initialize QEMU binfmt support
|
- name: Initialize QEMU binfmt support
|
||||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
||||||
with:
|
with:
|
||||||
platforms: "arm64,arm"
|
platforms: "arm64,arm"
|
||||||
|
|
||||||
# Start Docker Buildx
|
# Start Docker Buildx
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
|
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0
|
||||||
# https://github.com/moby/buildkit/issues/3969
|
# https://github.com/moby/buildkit/issues/3969
|
||||||
# Also set max parallelism to 3, the default of 4 breaks GitHub Actions and causes OOMKills
|
# Also set max parallelism to 3, the default of 4 breaks GitHub Actions and causes OOMKills
|
||||||
with:
|
with:
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Docker Hub
|
# Login to Docker Hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to GitHub Container Registry
|
# Login to GitHub Container Registry
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
|
|
||||||
# Login to Quay.io
|
# Login to Quay.io
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
@@ -165,7 +165,7 @@ jobs:
|
|||||||
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
echo "CONTAINER_REGISTRIES=${CONTAINER_REGISTRIES:+${CONTAINER_REGISTRIES},}localhost:5000/vaultwarden/server" | tee -a "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Bake ${{ matrix.base_image }} containers
|
- name: Bake ${{ matrix.base_image }} containers
|
||||||
uses: docker/bake-action@1c5f18a523c4c68524cfbc5161494d8bb5b29d20 # v5.0.1
|
uses: docker/bake-action@a4d7f0b5b91c14a296d792d4ec53a9db17f02e67 # v5.5.0
|
||||||
env:
|
env:
|
||||||
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
BASE_TAGS: "${{ env.BASE_TAGS }}"
|
||||||
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
SOURCE_COMMIT: "${{ env.SOURCE_COMMIT }}"
|
||||||
@@ -223,28 +223,28 @@ jobs:
|
|||||||
|
|
||||||
# Upload artifacts to Github Actions
|
# Upload artifacts to Github Actions
|
||||||
- name: "Upload amd64 artifact"
|
- name: "Upload amd64 artifact"
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' }}
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-amd64
|
||||||
path: vaultwarden-amd64
|
path: vaultwarden-amd64
|
||||||
|
|
||||||
- name: "Upload arm64 artifact"
|
- name: "Upload arm64 artifact"
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' }}
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-arm64
|
||||||
path: vaultwarden-arm64
|
path: vaultwarden-arm64
|
||||||
|
|
||||||
- name: "Upload armv7 artifact"
|
- name: "Upload armv7 artifact"
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' }}
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv7
|
||||||
path: vaultwarden-armv7
|
path: vaultwarden-armv7
|
||||||
|
|
||||||
- name: "Upload armv6 artifact"
|
- name: "Upload armv6 artifact"
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||||
if: ${{ matrix.base_image == 'alpine' }}
|
if: ${{ matrix.base_image == 'alpine' }}
|
||||||
with:
|
with:
|
||||||
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6
|
name: vaultwarden-${{ env.SOURCE_VERSION }}-linux-armv6
|
||||||
|
2
.github/workflows/trivy.yml
vendored
2
.github/workflows/trivy.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: Run Trivy vulnerability scanner
|
||||||
uses: aquasecurity/trivy-action@7c2007bcb556501da015201bcba5aa14069b74e2 # v0.23.0
|
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0
|
||||||
with:
|
with:
|
||||||
scan-type: repo
|
scan-type: repo
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
291
Cargo.lock
generated
291
Cargo.lock
generated
@@ -111,9 +111,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
|
checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -125,9 +125,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-executor"
|
name = "async-executor"
|
||||||
version = "1.12.0"
|
version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0"
|
checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
@@ -229,9 +229,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-signal"
|
name = "async-signal"
|
||||||
version = "0.2.8"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d"
|
checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io 2.3.3",
|
"async-io 2.3.3",
|
||||||
"async-lock 3.4.0",
|
"async-lock 3.4.0",
|
||||||
@@ -468,9 +468,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.16.1"
|
version = "1.16.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
|
checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -480,15 +480,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.6.0"
|
version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cached"
|
name = "cached"
|
||||||
version = "0.52.0"
|
version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8466736fe5dbcaf8b8ee24f9bbefe43c884dc3e9ff7178da70f55bffca1133c"
|
checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -496,17 +496,17 @@ dependencies = [
|
|||||||
"cached_proc_macro_types",
|
"cached_proc_macro_types",
|
||||||
"futures",
|
"futures",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"instant",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cached_proc_macro"
|
name = "cached_proc_macro"
|
||||||
version = "0.22.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "575f32e012222055211b70f5b0601f951f84523410a0e65c81f2744a6042450d"
|
checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -522,9 +522,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.106"
|
version = "1.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2"
|
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@@ -677,9 +677,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.9"
|
version = "0.20.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
|
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"darling_macro",
|
"darling_macro",
|
||||||
@@ -687,9 +687,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_core"
|
name = "darling_core"
|
||||||
version = "0.20.9"
|
version = "0.20.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
|
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fnv",
|
"fnv",
|
||||||
"ident_case",
|
"ident_case",
|
||||||
@@ -701,9 +701,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.20.9"
|
version = "0.20.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
|
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -793,9 +793,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel"
|
name = "diesel"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62d6dcd069e7b5fe49a302411f759d4cf1cf2c27fe798ef46fb8baefc053dd2b"
|
checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
@@ -817,9 +817,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel_derives"
|
name = "diesel_derives"
|
||||||
version = "2.2.1"
|
version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59de76a222c2b8059f789cbe07afbfd8deb8c31dd0bc2a21f85e256c1def8259"
|
checksum = "d6ff2be1e7312c858b2ef974f5c7089833ae57b5311b334b30923af58e5718d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"diesel_table_macro_syntax",
|
"diesel_table_macro_syntax",
|
||||||
"dsl_auto_type",
|
"dsl_auto_type",
|
||||||
@@ -877,9 +877,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dsl_auto_type"
|
name = "dsl_auto_type"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc"
|
checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"either",
|
"either",
|
||||||
@@ -907,9 +907,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email_address"
|
name = "email_address"
|
||||||
version = "0.2.5"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1019fa28f600f5b581b7a603d515c3f1635da041ca211b5055804788673abfe"
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -1041,9 +1041,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.30"
|
version = "1.0.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
@@ -1328,9 +1328,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "handlebars"
|
name = "handlebars"
|
||||||
version = "5.1.2"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
|
checksum = "5226a0e122dc74917f3a701484482bed3ee86d016c7356836abbaa033133a157"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pest",
|
"pest",
|
||||||
@@ -1504,9 +1504,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
@@ -1521,7 +1521,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1539,9 +1539,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
|
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -1563,16 +1563,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc"
|
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.5",
|
"h2 0.4.5",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -1589,9 +1589,9 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"hyper 1.4.0",
|
"hyper 1.4.1",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls 0.23.11",
|
"rustls 0.23.12",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.0",
|
"tokio-rustls 0.26.0",
|
||||||
@@ -1605,7 +1605,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"hyper 0.14.29",
|
"hyper 0.14.30",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
@@ -1619,7 +1619,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.4.0",
|
"hyper 1.4.1",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1629,16 +1629,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
|
checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.1",
|
||||||
"hyper 1.4.0",
|
"hyper 1.4.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.5.7",
|
"socket2 0.5.7",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1708,9 +1708,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@@ -1889,9 +1889,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.28.0"
|
version = "0.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
checksum = "d4588d65215825ee71ebff9e1c9982067833b1355d7546845ffdb3165cbd7456"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@@ -2033,13 +2033,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.11"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"hermit-abi 0.3.9",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2186,9 +2187,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.1"
|
version = "0.36.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
|
checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -2201,9 +2202,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.64"
|
version = "0.10.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -2242,9 +2243,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.102"
|
version = "0.9.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2524,9 +2525,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.6.0"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
@@ -2536,9 +2537,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pq-sys"
|
name = "pq-sys"
|
||||||
@@ -2628,9 +2632,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quoted_printable"
|
name = "quoted_printable"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
|
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r2d2"
|
name = "r2d2"
|
||||||
@@ -2675,18 +2679,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-cpuid"
|
name = "raw-cpuid"
|
||||||
version = "11.0.2"
|
version = "11.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd"
|
checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
|
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
]
|
]
|
||||||
@@ -2713,9 +2717,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.5"
|
version = "1.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -2780,7 +2784,7 @@ dependencies = [
|
|||||||
"h2 0.3.26",
|
"h2 0.3.26",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.29",
|
"hyper 0.14.30",
|
||||||
"hyper-tls 0.5.0",
|
"hyper-tls 0.5.0",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -2822,9 +2826,9 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.5",
|
"h2 0.4.5",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.4.0",
|
"hyper 1.4.1",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"hyper-tls 0.6.0",
|
"hyper-tls 0.6.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
@@ -2836,7 +2840,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pemfile 2.1.2",
|
"rustls-pemfile 2.1.3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@@ -2966,7 +2970,7 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"hyper 0.14.29",
|
"hyper 0.14.30",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3064,13 +3068,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.11"
|
version = "0.23.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
|
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.102.5",
|
"rustls-webpki 0.102.6",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -3086,9 +3090,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "2.1.2"
|
version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3096,9 +3100,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
@@ -3112,9 +3116,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.102.5"
|
version = "0.102.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
|
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3184,9 +3188,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.0"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
@@ -3197,9 +3201,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework-sys"
|
name = "security-framework-sys"
|
||||||
version = "2.11.0"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
|
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3243,20 +3247,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.120"
|
version = "1.0.122"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.6"
|
version = "0.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -3436,9 +3441,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.70"
|
version = "2.0.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16"
|
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3493,30 +3498,31 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.10.1"
|
version = "3.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand 2.1.0",
|
"fastrand 2.1.0",
|
||||||
|
"once_cell",
|
||||||
"rustix 0.38.34",
|
"rustix 0.38.34",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.61"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3592,28 +3598,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.38.0"
|
version = "1.39.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2 0.5.7",
|
"socket2 0.5.7",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.3.0"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3646,16 +3651,16 @@ version = "0.26.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls 0.23.11",
|
"rustls 0.23.12",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-socks"
|
name = "tokio-socks"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
|
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -3701,9 +3706,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.14"
|
version = "0.8.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
@@ -3713,18 +3718,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.6"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.15"
|
version = "0.22.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1"
|
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3943,9 +3948,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.9.1"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
|
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
@@ -4032,9 +4037,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
@@ -4156,6 +4161,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webauthn-rs"
|
name = "webauthn-rs"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -4177,9 +4192,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "6.0.1"
|
version = "6.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
|
checksum = "3d9c5ed668ee1f17edb3b627225343d210006a90bb1e3745ce1f30b1fb115075"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"home",
|
"home",
|
||||||
@@ -4211,11 +4226,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4270,6 +4285,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -4393,9 +4417,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.13"
|
version = "0.6.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
|
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -4457,6 +4481,7 @@ version = "0.7.35"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
26
Cargo.toml
26
Cargo.toml
@@ -3,7 +3,7 @@ name = "vaultwarden"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.78.0"
|
rust-version = "1.79.0"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
repository = "https://github.com/dani-garcia/vaultwarden"
|
repository = "https://github.com/dani-garcia/vaultwarden"
|
||||||
@@ -67,26 +67,26 @@ dashmap = "6.0.1"
|
|||||||
|
|
||||||
# Async futures
|
# Async futures
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
tokio = { version = "1.38.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
tokio = { version = "1.39.2", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||||
|
|
||||||
# A generic serialization/deserialization framework
|
# A generic serialization/deserialization framework
|
||||||
serde = { version = "1.0.204", features = ["derive"] }
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
serde_json = "1.0.120"
|
serde_json = "1.0.122"
|
||||||
|
|
||||||
# A safe, extensible ORM and Query builder
|
# A safe, extensible ORM and Query builder
|
||||||
diesel = { version = "2.2.1", features = ["chrono", "r2d2", "numeric"] }
|
diesel = { version = "2.2.2", features = ["chrono", "r2d2", "numeric"] }
|
||||||
diesel_migrations = "2.2.0"
|
diesel_migrations = "2.2.0"
|
||||||
diesel_logger = { version = "0.3.0", optional = true }
|
diesel_logger = { version = "0.3.0", optional = true }
|
||||||
|
|
||||||
# Bundled/Static SQLite
|
# Bundled/Static SQLite
|
||||||
libsqlite3-sys = { version = "0.28.0", features = ["bundled"], optional = true }
|
libsqlite3-sys = { version = "0.29.0", features = ["bundled"], optional = true }
|
||||||
|
|
||||||
# Crypto-related libraries
|
# Crypto-related libraries
|
||||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||||
ring = "0.17.8"
|
ring = "0.17.8"
|
||||||
|
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.9.1", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
|
|
||||||
# Date and time libraries
|
# Date and time libraries
|
||||||
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
|
chrono = { version = "0.4.38", features = ["clock", "serde"], default-features = false }
|
||||||
@@ -117,10 +117,10 @@ url = "2.5.2"
|
|||||||
# Email libraries
|
# Email libraries
|
||||||
lettre = { version = "0.11.7", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
lettre = { version = "0.11.7", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||||
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||||
email_address = "0.2.5"
|
email_address = "0.2.9"
|
||||||
|
|
||||||
# HTML Template library
|
# HTML Template library
|
||||||
handlebars = { version = "5.1.2", features = ["dir_source"] }
|
handlebars = { version = "6.0.0", features = ["dir_source"] }
|
||||||
|
|
||||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||||
reqwest = { version = "0.12.5", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
reqwest = { version = "0.12.5", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||||
@@ -128,19 +128,19 @@ hickory-resolver = "0.24.1"
|
|||||||
|
|
||||||
# Favicon extraction libraries
|
# Favicon extraction libraries
|
||||||
html5gum = "0.5.7"
|
html5gum = "0.5.7"
|
||||||
regex = { version = "1.10.5", features = ["std", "perf", "unicode-perl"], default-features = false }
|
regex = { version = "1.10.6", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
bytes = "1.6.0"
|
bytes = "1.7.1"
|
||||||
|
|
||||||
# Cache function results (Used for version check and favicon fetching)
|
# Cache function results (Used for version check and favicon fetching)
|
||||||
cached = { version = "0.52.0", features = ["async"] }
|
cached = { version = "0.53.1", features = ["async"] }
|
||||||
|
|
||||||
# Used for custom short lived cookie jar during favicon extraction
|
# Used for custom short lived cookie jar during favicon extraction
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
cookie_store = "0.21.0"
|
cookie_store = "0.21.0"
|
||||||
|
|
||||||
# Used by U2F, JWT and PostgreSQL
|
# Used by U2F, JWT and PostgreSQL
|
||||||
openssl = "0.10.64"
|
openssl = "0.10.66"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
@@ -155,7 +155,7 @@ semver = "1.0.23"
|
|||||||
# Allow overriding the default memory allocator
|
# Allow overriding the default memory allocator
|
||||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||||
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
||||||
which = "6.0.1"
|
which = "6.0.2"
|
||||||
|
|
||||||
# Argon2 library with support for the PHC format
|
# Argon2 library with support for the PHC format
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
vault_version: "v2024.5.1b"
|
vault_version: "v2024.6.2b"
|
||||||
vault_image_digest: "sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375"
|
vault_image_digest: "sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55"
|
||||||
# Cross Compile Docker Helper Scripts v1.4.0
|
# Cross Compile Docker Helper Scripts v1.4.0
|
||||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
xx_image_digest: "sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4"
|
xx_image_digest: "sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4"
|
||||||
rust_version: 1.79.0 # Rust version to be used
|
rust_version: 1.80.1 # Rust version to be used
|
||||||
debian_version: bookworm # Debian release name to be used
|
debian_version: bookworm # Debian release name to be used
|
||||||
alpine_version: "3.20" # Alpine version to be used
|
alpine_version: "3.20" # Alpine version to be used
|
||||||
# For which platforms/architectures will we try to build images
|
# For which platforms/architectures will we try to build images
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform
|
||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
||||||
@@ -18,27 +19,27 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.5.1b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.5.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2b
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375]
|
# [docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55
|
||||||
# [docker.io/vaultwarden/web-vault:v2024.5.1b]
|
# [docker.io/vaultwarden/web-vault:v2024.6.2b]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 as vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.79.0 as build_amd64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:x86_64-musl-stable-1.80.1 AS build_amd64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.79.0 as build_arm64
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:aarch64-musl-stable-1.80.1 AS build_arm64
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.79.0 as build_armv7
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:armv7-musleabihf-stable-1.80.1 AS build_armv7
|
||||||
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.79.0 as build_armv6
|
FROM --platform=linux/amd64 ghcr.io/blackdex/rust-musl:arm-musleabi-stable-1.80.1 AS build_armv6
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=linux/amd64 build_${TARGETARCH}${TARGETVARIANT} as build
|
FROM --platform=linux/amd64 build_${TARGETARCH}${TARGETVARIANT} AS build
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -142,7 +143,6 @@ RUN mkdir /data && \
|
|||||||
|
|
||||||
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
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform
|
||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
||||||
@@ -18,15 +19,15 @@
|
|||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2024.5.1b
|
# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2b
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.5.1b
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2b
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375]
|
# [docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55
|
||||||
# [docker.io/vaultwarden/web-vault:v2024.5.1b]
|
# [docker.io/vaultwarden/web-vault:v2024.6.2b]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:1a867b4b175e85fc8602314bd83bc263c76c49787031704f16a2915567725375 as vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:25a8b48792a3d2c16ddaea493eee93a0b6785648f2d8782c7537d198cb41be55 AS vault
|
||||||
|
|
||||||
########################## Cross Compile Docker Helper Scripts ##########################
|
########################## Cross Compile Docker Helper Scripts ##########################
|
||||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||||
@@ -35,7 +36,7 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:0cd3f05c72d6c9b038eb1
|
|||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.79.0-slim-bookworm as build
|
FROM --platform=$BUILDPLATFORM docker.io/library/rust:1.80.1-slim-bookworm AS build
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
@@ -185,7 +186,6 @@ RUN mkdir /data && \
|
|||||||
|
|
||||||
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
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform
|
||||||
|
|
||||||
# This file was generated using a Jinja2 template.
|
# This file was generated using a Jinja2 template.
|
||||||
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
# Please make your changes in `DockerSettings.yaml` or `Dockerfile.j2` and then `make`
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
# $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }}
|
||||||
# [docker.io/vaultwarden/web-vault:{{ vault_version }}]
|
# [docker.io/vaultwarden/web-vault:{{ vault_version }}]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} as vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault
|
||||||
|
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
########################## Cross Compile Docker Helper Scripts ##########################
|
########################## Cross Compile Docker Helper Scripts ##########################
|
||||||
@@ -38,13 +39,13 @@ FROM --platform=linux/amd64 docker.io/tonistiigi/xx@{{ xx_image_digest }} AS xx
|
|||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
## And for Alpine we define all build images here, they will only be loaded when actually used
|
## And for Alpine we define all build images here, they will only be loaded when actually used
|
||||||
{% for arch in build_stage_image[base].arch_image %}
|
{% for arch in build_stage_image[base].arch_image %}
|
||||||
FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].arch_image[arch] }} as build_{{ arch }}
|
FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].arch_image[arch] }} AS build_{{ arch }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
########################## BUILD IMAGE ##########################
|
########################## BUILD IMAGE ##########################
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].image }} as build
|
FROM --platform={{ build_stage_image[base].platform }} {{ build_stage_image[base].image }} AS build
|
||||||
{% if base == "debian" %}
|
{% if base == "debian" %}
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -229,7 +230,6 @@ RUN mkdir /data && \
|
|||||||
|
|
||||||
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
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_duo_ctx;
|
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE twofactor_duo_ctx (
|
||||||
|
state VARCHAR(64) NOT NULL,
|
||||||
|
user_email VARCHAR(255) NOT NULL,
|
||||||
|
nonce VARCHAR(64) NOT NULL,
|
||||||
|
exp BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (state)
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_duo_ctx;
|
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE twofactor_duo_ctx (
|
||||||
|
state VARCHAR(64) NOT NULL,
|
||||||
|
user_email VARCHAR(255) NOT NULL,
|
||||||
|
nonce VARCHAR(64) NOT NULL,
|
||||||
|
exp BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (state)
|
||||||
|
);
|
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE twofactor_duo_ctx;
|
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE twofactor_duo_ctx (
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
user_email TEXT NOT NULL,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
exp INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (state)
|
||||||
|
);
|
@@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.79.0"
|
channel = "1.80.1"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use reqwest::Method;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -17,14 +18,14 @@ use crate::{
|
|||||||
core::{log_event, two_factor},
|
core::{log_event, two_factor},
|
||||||
unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,
|
unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,
|
||||||
},
|
},
|
||||||
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
error::{Error, MapResult},
|
error::{Error, MapResult},
|
||||||
|
http_client::make_http_request,
|
||||||
mail,
|
mail,
|
||||||
util::{
|
util::{
|
||||||
container_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client,
|
container_base_image, format_naive_datetime_local, get_display_size, is_running_in_container, NumberOrString,
|
||||||
is_running_in_container, NumberOrString,
|
|
||||||
},
|
},
|
||||||
CONFIG, VERSION,
|
CONFIG, VERSION,
|
||||||
};
|
};
|
||||||
@@ -168,7 +169,12 @@ struct LoginForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<data>")]
|
#[post("/", data = "<data>")]
|
||||||
fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> {
|
fn post_admin_login(
|
||||||
|
data: Form<LoginForm>,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
ip: ClientIp,
|
||||||
|
secure: Secure,
|
||||||
|
) -> Result<Redirect, AdminResponse> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
let redirect = data.redirect;
|
let redirect = data.redirect;
|
||||||
|
|
||||||
@@ -192,7 +198,8 @@ fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp
|
|||||||
.path(admin_path())
|
.path(admin_path())
|
||||||
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime()))
|
.max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime()))
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Strict)
|
||||||
.http_only(true);
|
.http_only(true)
|
||||||
|
.secure(secure.https);
|
||||||
|
|
||||||
cookies.add(cookie);
|
cookies.add(cookie);
|
||||||
if let Some(redirect) = redirect {
|
if let Some(redirect) = redirect {
|
||||||
@@ -594,15 +601,15 @@ struct TimeApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
|
||||||
let json_api = get_reqwest_client();
|
Ok(make_http_request(Method::GET, url)?.send().await?.error_for_status()?.json::<T>().await?)
|
||||||
|
|
||||||
Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn has_http_access() -> bool {
|
async fn has_http_access() -> bool {
|
||||||
let http_access = get_reqwest_client();
|
let req = match make_http_request(Method::HEAD, "https://github.com/dani-garcia/vaultwarden") {
|
||||||
|
Ok(r) => r,
|
||||||
match http_access.head("https://github.com/dani-garcia/vaultwarden").send().await {
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
match req.send().await {
|
||||||
Ok(r) => r.status().is_success(),
|
Ok(r) => r.status().is_success(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
@@ -208,6 +208,7 @@ pub struct CipherData {
|
|||||||
// Folder id is not included in import
|
// Folder id is not included in import
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
// TODO: Some of these might appear all the time, no need for Option
|
// TODO: Some of these might appear all the time, no need for Option
|
||||||
|
#[serde(alias = "organizationID")]
|
||||||
pub organization_id: Option<String>,
|
pub organization_id: Option<String>,
|
||||||
|
|
||||||
key: Option<String>,
|
key: Option<String>,
|
||||||
@@ -286,6 +287,10 @@ async fn post_ciphers_create(
|
|||||||
if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() {
|
if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() {
|
||||||
err!("You must select at least one collection.");
|
err!("You must select at least one collection.");
|
||||||
}
|
}
|
||||||
|
// reverse sanity check to prevent corruptions
|
||||||
|
if !data.collection_ids.is_empty() && data.cipher.organization_id.is_none() {
|
||||||
|
err!("The client has not provided an organization id!");
|
||||||
|
}
|
||||||
|
|
||||||
// This check is usually only needed in update_cipher_from_data(), but we
|
// This check is usually only needed in update_cipher_from_data(), but we
|
||||||
// need it here as well to avoid creating an empty cipher in the call to
|
// need it here as well to avoid creating an empty cipher in the call to
|
||||||
@@ -377,8 +382,9 @@ pub async fn update_cipher_from_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(note) = &data.notes {
|
if let Some(note) = &data.notes {
|
||||||
if note.len() > 10_000 {
|
let max_note_size = CONFIG._max_note_size();
|
||||||
err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.")
|
if note.len() > max_note_size {
|
||||||
|
err!(format!("The field Notes exceeds the maximum encrypted value length of {max_note_size} characters."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ pub use accounts::purge_auth_requests;
|
|||||||
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType};
|
||||||
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
|
||||||
pub use events::{event_cleanup_job, log_event, log_user_event};
|
pub use events::{event_cleanup_job, log_event, log_user_event};
|
||||||
|
use reqwest::Method;
|
||||||
pub use sends::purge_sends;
|
pub use sends::purge_sends;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
@@ -53,7 +54,8 @@ use crate::{
|
|||||||
auth::Headers,
|
auth::Headers,
|
||||||
db::DbConn,
|
db::DbConn,
|
||||||
error::Error,
|
error::Error,
|
||||||
util::{get_reqwest_client, parse_experimental_client_feature_flags},
|
http_client::make_http_request,
|
||||||
|
util::parse_experimental_client_feature_flags,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -139,9 +141,7 @@ async fn hibp_breach(username: &str) -> JsonResult {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||||
let hibp_client = get_reqwest_client();
|
let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?;
|
||||||
|
|
||||||
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send().await?;
|
|
||||||
|
|
||||||
// If we get a 404, return a 404, it means no breached accounts
|
// If we get a 404, return a 404, it means no breached accounts
|
||||||
if res.status() == 404 {
|
if res.status() == 404 {
|
||||||
@@ -190,6 +190,8 @@ fn config() -> Json<Value> {
|
|||||||
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags());
|
||||||
// Force the new key rotation feature
|
// Force the new key rotation feature
|
||||||
feature_states.insert("key-rotation-improvements".to_string(), true);
|
feature_states.insert("key-rotation-improvements".to_string(), true);
|
||||||
|
feature_states.insert("flexible-collections-v-1".to_string(), false);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
// Note: The clients use this version to handle backwards compatibility concerns
|
// Note: The clients use this version to handle backwards compatibility concerns
|
||||||
// This means they expect a version that closely matches the Bitwarden server version
|
// This means they expect a version that closely matches the Bitwarden server version
|
||||||
|
@@ -2,6 +2,7 @@ use num_traits::FromPrimitive;
|
|||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
@@ -39,6 +40,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
delete_organization_collection,
|
delete_organization_collection,
|
||||||
post_organization_collection_delete,
|
post_organization_collection_delete,
|
||||||
bulk_delete_organization_collections,
|
bulk_delete_organization_collections,
|
||||||
|
post_bulk_collections,
|
||||||
get_org_details,
|
get_org_details,
|
||||||
get_org_users,
|
get_org_users,
|
||||||
send_invite,
|
send_invite,
|
||||||
@@ -65,6 +67,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
import,
|
import,
|
||||||
post_org_keys,
|
post_org_keys,
|
||||||
get_organization_keys,
|
get_organization_keys,
|
||||||
|
get_organization_public_key,
|
||||||
bulk_public_keys,
|
bulk_public_keys,
|
||||||
deactivate_organization_user,
|
deactivate_organization_user,
|
||||||
bulk_deactivate_organization_user,
|
bulk_deactivate_organization_user,
|
||||||
@@ -749,12 +752,19 @@ struct OrgIdData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/organization-details?<data..>")]
|
#[get("/ciphers/organization-details?<data..>")]
|
||||||
async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> Json<Value> {
|
async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
Json(json!({
|
if UserOrganization::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn)
|
||||||
|
.await
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
err_code!("Resource not found.", rocket::http::Status::NotFound.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await,
|
"data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"continuationToken": null,
|
"continuationToken": null,
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value {
|
async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut DbConn) -> Value {
|
||||||
@@ -844,7 +854,8 @@ struct InviteData {
|
|||||||
groups: Vec<String>,
|
groups: Vec<String>,
|
||||||
r#type: NumberOrString,
|
r#type: NumberOrString,
|
||||||
collections: Option<Vec<CollectionData>>,
|
collections: Option<Vec<CollectionData>>,
|
||||||
access_all: Option<bool>,
|
#[serde(default)]
|
||||||
|
access_all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/users/invite", data = "<data>")]
|
#[post("/organizations/<org_id>/users/invite", data = "<data>")]
|
||||||
@@ -896,7 +907,7 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut new_user = UserOrganization::new(user.uuid.clone(), String::from(org_id));
|
let mut new_user = UserOrganization::new(user.uuid.clone(), String::from(org_id));
|
||||||
let access_all = data.access_all.unwrap_or(false);
|
let access_all = data.access_all;
|
||||||
new_user.access_all = access_all;
|
new_user.access_all = access_all;
|
||||||
new_user.atype = new_type;
|
new_user.atype = new_type;
|
||||||
new_user.status = user_org_status;
|
new_user.status = user_org_status;
|
||||||
@@ -997,14 +1008,6 @@ async fn reinvite_user(org_id: &str, user_org: &str, headers: AdminHeaders, mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult {
|
async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
if !CONFIG.invitations_allowed() {
|
|
||||||
err!("Invitations are not allowed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
|
||||||
err!("SMTP is not configured.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_org = match UserOrganization::find_by_uuid(user_org, conn).await {
|
let user_org = match UserOrganization::find_by_uuid(user_org, conn).await {
|
||||||
Some(user_org) => user_org,
|
Some(user_org) => user_org,
|
||||||
None => err!("The user hasn't been invited to the organization."),
|
None => err!("The user hasn't been invited to the organization."),
|
||||||
@@ -1019,6 +1022,10 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
|
|||||||
None => err!("User not found."),
|
None => err!("User not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !CONFIG.invitations_allowed() && user.password_hash.is_empty() {
|
||||||
|
err!("Invitations are not allowed.")
|
||||||
|
}
|
||||||
|
|
||||||
let org_name = match Organization::find_by_uuid(org_id, conn).await {
|
let org_name = match Organization::find_by_uuid(org_id, conn).await {
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
None => err!("Error looking up organization."),
|
None => err!("Error looking up organization."),
|
||||||
@@ -1034,9 +1041,14 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
|
|||||||
Some(invited_by_email.to_string()),
|
Some(invited_by_email.to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else if user.password_hash.is_empty() {
|
||||||
let invitation = Invitation::new(&user.email);
|
let invitation = Invitation::new(&user.email);
|
||||||
invitation.save(conn).await?;
|
invitation.save(conn).await?;
|
||||||
|
} else {
|
||||||
|
let _ = Invitation::take(&user.email, conn).await;
|
||||||
|
let mut user_org = user_org;
|
||||||
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
|
user_org.save(conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1296,6 +1308,7 @@ struct EditUserData {
|
|||||||
r#type: NumberOrString,
|
r#type: NumberOrString,
|
||||||
collections: Option<Vec<CollectionData>>,
|
collections: Option<Vec<CollectionData>>,
|
||||||
groups: Option<Vec<String>>,
|
groups: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
access_all: bool,
|
access_all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,6 +1641,66 @@ async fn post_org_import(
|
|||||||
user.update_revision(&mut conn).await
|
user.update_revision(&mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct BulkCollectionsData {
|
||||||
|
organization_id: String,
|
||||||
|
cipher_ids: Vec<String>,
|
||||||
|
collection_ids: HashSet<String>,
|
||||||
|
remove_collections: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This endpoint is only reachable via the organization view, therefor this endpoint is located here
|
||||||
|
// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates
|
||||||
|
#[post("/ciphers/bulk-collections", data = "<data>")]
|
||||||
|
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
||||||
|
let data: BulkCollectionsData = data.into_inner();
|
||||||
|
|
||||||
|
// This feature does not seem to be active on all the clients
|
||||||
|
// To prevent future issues, add a check to block a call when this is set to true
|
||||||
|
if data.remove_collections {
|
||||||
|
err!("Bulk removing of collections is not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the collection available to the user in one query
|
||||||
|
// Also filter based upon the provided collections
|
||||||
|
let user_collections: HashMap<String, Collection> =
|
||||||
|
Collection::find_by_organization_and_user_uuid(&data.organization_id, &headers.user.uuid, &mut conn)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|c| {
|
||||||
|
if data.collection_ids.contains(&c.uuid) {
|
||||||
|
Some((c.uuid.clone(), c))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Verify if all the collections requested exists and are writeable for the user, else abort
|
||||||
|
for collection_uuid in &data.collection_ids {
|
||||||
|
match user_collections.get(collection_uuid) {
|
||||||
|
Some(collection) if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await => (),
|
||||||
|
_ => err_code!("Resource not found", "User does not have access to a collection", 404),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for cipher_id in data.cipher_ids.iter() {
|
||||||
|
// Only act on existing cipher uuid's
|
||||||
|
// Do not abort the operation just ignore it, it could be a cipher was just deleted for example
|
||||||
|
if let Some(cipher) = Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &mut conn).await {
|
||||||
|
if cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await {
|
||||||
|
for collection in &data.collection_ids {
|
||||||
|
CollectionCipher::save(&cipher.uuid, collection, &mut conn).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies")]
|
#[get("/organizations/<org_id>/policies")]
|
||||||
async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
|
async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
|
||||||
let policies = OrgPolicy::find_by_org(org_id, &mut conn).await;
|
let policies = OrgPolicy::find_by_org(org_id, &mut conn).await;
|
||||||
@@ -1642,6 +1715,13 @@ async fn list_policies(org_id: &str, _headers: AdminHeaders, mut conn: DbConn) -
|
|||||||
|
|
||||||
#[get("/organizations/<org_id>/policies/token?<token>")]
|
#[get("/organizations/<org_id>/policies/token?<token>")]
|
||||||
async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> JsonResult {
|
async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> JsonResult {
|
||||||
|
// web-vault 2024.6.2 seems to send these values and cause logs to output errors
|
||||||
|
// Catch this and prevent errors in the logs
|
||||||
|
// TODO: CleanUp after 2024.6.x is not used anymore.
|
||||||
|
if org_id == "undefined" && token == "undefined" {
|
||||||
|
return Ok(Json(json!({})));
|
||||||
|
}
|
||||||
|
|
||||||
let invite = crate::auth::decode_invite(token)?;
|
let invite = crate::auth::decode_invite(token)?;
|
||||||
|
|
||||||
let invite_org_id = match invite.org_id {
|
let invite_org_id = match invite.org_id {
|
||||||
@@ -2222,7 +2302,8 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct GroupRequest {
|
struct GroupRequest {
|
||||||
name: String,
|
name: String,
|
||||||
access_all: Option<bool>,
|
#[serde(default)]
|
||||||
|
access_all: bool,
|
||||||
external_id: Option<String>,
|
external_id: Option<String>,
|
||||||
collections: Vec<SelectionReadOnly>,
|
collections: Vec<SelectionReadOnly>,
|
||||||
users: Vec<String>,
|
users: Vec<String>,
|
||||||
@@ -2230,17 +2311,12 @@ struct GroupRequest {
|
|||||||
|
|
||||||
impl GroupRequest {
|
impl GroupRequest {
|
||||||
pub fn to_group(&self, organizations_uuid: &str) -> Group {
|
pub fn to_group(&self, organizations_uuid: &str) -> Group {
|
||||||
Group::new(
|
Group::new(String::from(organizations_uuid), self.name.clone(), self.access_all, self.external_id.clone())
|
||||||
String::from(organizations_uuid),
|
|
||||||
self.name.clone(),
|
|
||||||
self.access_all.unwrap_or(false),
|
|
||||||
self.external_id.clone(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_group(&self, mut group: Group) -> Group {
|
pub fn update_group(&self, mut group: Group) -> Group {
|
||||||
group.name.clone_from(&self.name);
|
group.name.clone_from(&self.name);
|
||||||
group.access_all = self.access_all.unwrap_or(false);
|
group.access_all = self.access_all;
|
||||||
// Group Updates do not support changing the external_id
|
// Group Updates do not support changing the external_id
|
||||||
// These input fields are in a disabled state, and can only be updated/added via ldap_import
|
// These input fields are in a disabled state, and can only be updated/added via ldap_import
|
||||||
|
|
||||||
@@ -2680,20 +2756,29 @@ struct OrganizationUserResetPasswordRequest {
|
|||||||
key: String,
|
key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/keys")]
|
// Upstrem reports this is the renamed endpoint instead of `/keys`
|
||||||
async fn get_organization_keys(org_id: &str, mut conn: DbConn) -> JsonResult {
|
// But the clients do not seem to use this at all
|
||||||
|
// Just add it here in case they will
|
||||||
|
#[get("/organizations/<org_id>/public-key")]
|
||||||
|
async fn get_organization_public_key(org_id: &str, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let org = match Organization::find_by_uuid(org_id, &mut conn).await {
|
let org = match Organization::find_by_uuid(org_id, &mut conn).await {
|
||||||
Some(organization) => organization,
|
Some(organization) => organization,
|
||||||
None => err!("Organization not found"),
|
None => err!("Organization not found"),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"object": "organizationKeys",
|
"object": "organizationPublicKey",
|
||||||
"publicKey": org.public_key,
|
"publicKey": org.public_key,
|
||||||
"privateKey": org.private_key,
|
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
|
||||||
|
// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468
|
||||||
|
#[get("/organizations/<org_id>/keys")]
|
||||||
|
async fn get_organization_keys(org_id: &str, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
|
get_organization_public_key(org_id, headers, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
#[put("/organizations/<org_id>/users/<org_user_id>/reset-password", data = "<data>")]
|
#[put("/organizations/<org_id>/users/<org_user_id>/reset-password", data = "<data>")]
|
||||||
async fn put_reset_password(
|
async fn put_reset_password(
|
||||||
org_id: &str,
|
org_id: &str,
|
||||||
|
@@ -349,7 +349,15 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L243
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SendFileData {
|
||||||
|
id: String,
|
||||||
|
size: u64,
|
||||||
|
fileName: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250
|
||||||
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
||||||
async fn post_send_file_v2_data(
|
async fn post_send_file_v2_data(
|
||||||
send_uuid: &str,
|
send_uuid: &str,
|
||||||
@@ -367,15 +375,55 @@ async fn post_send_file_v2_data(
|
|||||||
err!("Send not found. Unable to save the file.")
|
err!("Send not found. Unable to save the file.")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if send.atype != SendType::File as i32 {
|
||||||
|
err!("Send is not a file type send.");
|
||||||
|
}
|
||||||
|
|
||||||
let Some(send_user_id) = &send.user_uuid else {
|
let Some(send_user_id) = &send.user_uuid else {
|
||||||
err!("Sends are only supported for users at the moment")
|
err!("Sends are only supported for users at the moment.")
|
||||||
};
|
};
|
||||||
|
|
||||||
if send_user_id != &headers.user.uuid {
|
if send_user_id != &headers.user.uuid {
|
||||||
err!("Send doesn't belong to user");
|
err!("Send doesn't belong to user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(send_data) = serde_json::from_str::<SendFileData>(&send.data) else {
|
||||||
|
err!("Unable to decode send data as json.")
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.data.raw_name() {
|
||||||
|
Some(raw_file_name) if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName => (),
|
||||||
|
Some(raw_file_name) => err!(
|
||||||
|
"Send file name does not match.",
|
||||||
|
format!(
|
||||||
|
"Expected file name '{}' got '{}'",
|
||||||
|
send_data.fileName,
|
||||||
|
raw_file_name.dangerous_unsafe_unsanitized_raw()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_ => err!("Send file name does not match or is not provided."),
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_id != send_data.id {
|
||||||
|
err!("Send file does not match send data.", format!("Expected id {} got {file_id}", send_data.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(size) = data.data.len().to_u64() else {
|
||||||
|
err!("Send file size overflow.");
|
||||||
|
};
|
||||||
|
|
||||||
|
if size != send_data.size {
|
||||||
|
err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size));
|
||||||
}
|
}
|
||||||
|
|
||||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid);
|
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid);
|
||||||
let file_path = folder_path.join(file_id);
|
let file_path = folder_path.join(file_id);
|
||||||
|
|
||||||
|
// Check if the file already exists, if that is the case do not overwrite it
|
||||||
|
if tokio::fs::metadata(&file_path).await.is_ok() {
|
||||||
|
err!("Send file has already been uploaded.", format!("File {file_path:?} already exists"))
|
||||||
|
}
|
||||||
|
|
||||||
tokio::fs::create_dir_all(&folder_path).await?;
|
tokio::fs::create_dir_all(&folder_path).await?;
|
||||||
|
|
||||||
if let Err(_err) = data.data.persist_to(&file_path).await {
|
if let Err(_err) = data.data.persist_to(&file_path).await {
|
||||||
|
@@ -15,7 +15,7 @@ use crate::{
|
|||||||
DbConn,
|
DbConn,
|
||||||
},
|
},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
util::get_reqwest_client,
|
http_client::make_http_request,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,10 +210,7 @@ async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData)
|
|||||||
|
|
||||||
let m = Method::from_str(method).unwrap_or_default();
|
let m = Method::from_str(method).unwrap_or_default();
|
||||||
|
|
||||||
let client = get_reqwest_client();
|
make_http_request(m, &url)?
|
||||||
|
|
||||||
client
|
|
||||||
.request(m, &url)
|
|
||||||
.basic_auth(username, Some(password))
|
.basic_auth(username, Some(password))
|
||||||
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
||||||
.header(header::DATE, date)
|
.header(header::DATE, date)
|
||||||
@@ -255,7 +252,7 @@ async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// let (ik, sk, ak, host) = get_duo_keys();
|
// let (ik, sk, ak, host) = get_duo_keys();
|
||||||
async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
|
pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
|
||||||
let data = match User::find_by_mail(email, conn).await {
|
let data = match User::find_by_mail(email, conn).await {
|
||||||
Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),
|
Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),
|
||||||
_ => DuoData::global(),
|
_ => DuoData::global(),
|
||||||
|
498
src/api/core/two_factor/duo_oidc.rs
Normal file
498
src/api/core/two_factor/duo_oidc.rs
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use reqwest::{header, StatusCode};
|
||||||
|
use ring::digest::{digest, Digest, SHA512_256};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
|
||||||
|
crypto,
|
||||||
|
db::{
|
||||||
|
models::{EventType, TwoFactorDuoContext},
|
||||||
|
DbConn, DbPool,
|
||||||
|
},
|
||||||
|
error::Error,
|
||||||
|
http_client::make_http_request,
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
// The location on this service that Duo should redirect users to. For us, this is a bridge
|
||||||
|
// built in to the Bitwarden clients.
|
||||||
|
// See: https://github.com/bitwarden/clients/blob/main/apps/web/src/connectors/duo-redirect.ts
|
||||||
|
const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html";
|
||||||
|
|
||||||
|
// Number of seconds that a JWT we generate for Duo should be valid for.
|
||||||
|
const JWT_VALIDITY_SECS: i64 = 300;
|
||||||
|
|
||||||
|
// Number of seconds that a Duo context stored in the database should be valid for.
|
||||||
|
const CTX_VALIDITY_SECS: i64 = 300;
|
||||||
|
|
||||||
|
// Expected algorithm used by Duo to sign JWTs.
|
||||||
|
const DUO_RESP_SIGNATURE_ALG: Algorithm = Algorithm::HS512;
|
||||||
|
|
||||||
|
// Signature algorithm we're using to sign JWTs for Duo. Must be either HS512 or HS256.
|
||||||
|
const JWT_SIGNATURE_ALG: Algorithm = Algorithm::HS512;
|
||||||
|
|
||||||
|
// Size of random strings for state and nonce. Must be at least 16 characters and at most 1024 characters.
|
||||||
|
// If increasing this above 64, also increase the size of the twofactor_duo_ctx.state and
|
||||||
|
// twofactor_duo_ctx.nonce database columns for postgres and mariadb.
|
||||||
|
const STATE_LENGTH: usize = 64;
|
||||||
|
|
||||||
|
// client_assertion payload for health checks and obtaining MFA results.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ClientAssertion {
|
||||||
|
pub iss: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub aud: String,
|
||||||
|
pub exp: i64,
|
||||||
|
pub jti: String,
|
||||||
|
pub iat: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorization request payload sent with clients to Duo for MFA
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct AuthorizationRequest {
|
||||||
|
pub response_type: String,
|
||||||
|
pub scope: String,
|
||||||
|
pub exp: i64,
|
||||||
|
pub client_id: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
pub state: String,
|
||||||
|
pub duo_uname: String,
|
||||||
|
pub iss: String,
|
||||||
|
pub aud: String,
|
||||||
|
pub nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duo service health check responses
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum HealthCheckResponse {
|
||||||
|
HealthOK {
|
||||||
|
stat: String,
|
||||||
|
},
|
||||||
|
HealthFail {
|
||||||
|
message: String,
|
||||||
|
message_detail: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer structure of response when exchanging authz code for MFA results
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct IdTokenResponse {
|
||||||
|
id_token: String, // IdTokenClaims
|
||||||
|
access_token: String,
|
||||||
|
expires_in: i64,
|
||||||
|
token_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner structure of IdTokenResponse.id_token
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct IdTokenClaims {
|
||||||
|
preferred_username: String,
|
||||||
|
nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duo OIDC Authorization Client
|
||||||
|
// See https://duo.com/docs/oauthapi
|
||||||
|
struct DuoClient {
|
||||||
|
client_id: String, // Duo Client ID (DuoData.ik)
|
||||||
|
client_secret: String, // Duo Client Secret (DuoData.sk)
|
||||||
|
api_host: String, // Duo API hostname (DuoData.host)
|
||||||
|
redirect_uri: String, // URL in this application clients should call for MFA verification
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DuoClient {
|
||||||
|
// Construct a new DuoClient
|
||||||
|
fn new(client_id: String, client_secret: String, api_host: String, redirect_uri: String) -> DuoClient {
|
||||||
|
DuoClient {
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
api_host,
|
||||||
|
redirect_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a client assertion for health checks and authorization code exchange.
|
||||||
|
fn new_client_assertion(&self, url: &str) -> ClientAssertion {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
let jwt_id = crypto::get_random_string_alphanum(STATE_LENGTH);
|
||||||
|
|
||||||
|
ClientAssertion {
|
||||||
|
iss: self.client_id.clone(),
|
||||||
|
sub: self.client_id.clone(),
|
||||||
|
aud: url.to_string(),
|
||||||
|
exp: now + JWT_VALIDITY_SECS,
|
||||||
|
jti: jwt_id,
|
||||||
|
iat: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a serde-serializable struct, attempt to encode it as a JWT
|
||||||
|
fn encode_duo_jwt<T: Serialize>(&self, jwt_payload: T) -> Result<String, Error> {
|
||||||
|
match jsonwebtoken::encode(
|
||||||
|
&Header::new(JWT_SIGNATURE_ALG),
|
||||||
|
&jwt_payload,
|
||||||
|
&EncodingKey::from_secret(self.client_secret.as_bytes()),
|
||||||
|
) {
|
||||||
|
Ok(token) => Ok(token),
|
||||||
|
Err(e) => err!(format!("Error encoding Duo JWT: {e:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "required" health check to verify the integration is configured and Duo's services
|
||||||
|
// are up.
|
||||||
|
// https://duo.com/docs/oauthapi#health-check
|
||||||
|
async fn health_check(&self) -> Result<(), Error> {
|
||||||
|
let health_check_url: String = format!("https://{}/oauth/v1/health_check", self.api_host);
|
||||||
|
|
||||||
|
let jwt_payload = self.new_client_assertion(&health_check_url);
|
||||||
|
|
||||||
|
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut post_body = HashMap::new();
|
||||||
|
post_body.insert("client_assertion", token);
|
||||||
|
post_body.insert("client_id", self.client_id.clone());
|
||||||
|
|
||||||
|
let res = match make_http_request(reqwest::Method::POST, &health_check_url)?
|
||||||
|
.header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)")
|
||||||
|
.form(&post_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Error requesting Duo health check: {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: HealthCheckResponse = match res.json::<HealthCheckResponse>().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Duo health check response decode error: {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let health_stat: String = match response {
|
||||||
|
HealthCheckResponse::HealthOK {
|
||||||
|
stat,
|
||||||
|
} => stat,
|
||||||
|
HealthCheckResponse::HealthFail {
|
||||||
|
message,
|
||||||
|
message_detail,
|
||||||
|
} => err!(format!("Duo health check FAIL response, msg: {}, detail: {}", message, message_detail)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if health_stat != "OK" {
|
||||||
|
err!(format!("Duo health check failed, got OK-like body with stat {health_stat}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs the URL for the authorization request endpoint on Duo's service.
|
||||||
|
// Clients are sent here to continue authentication.
|
||||||
|
// https://duo.com/docs/oauthapi#authorization-request
|
||||||
|
fn make_authz_req_url(&self, duo_username: &str, state: String, nonce: String) -> Result<String, Error> {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
|
let jwt_payload = AuthorizationRequest {
|
||||||
|
response_type: String::from("code"),
|
||||||
|
scope: String::from("openid"),
|
||||||
|
exp: now + JWT_VALIDITY_SECS,
|
||||||
|
client_id: self.client_id.clone(),
|
||||||
|
redirect_uri: self.redirect_uri.clone(),
|
||||||
|
state,
|
||||||
|
duo_uname: String::from(duo_username),
|
||||||
|
iss: self.client_id.clone(),
|
||||||
|
aud: format!("https://{}", self.api_host),
|
||||||
|
nonce,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let authz_endpoint = format!("https://{}/oauth/v1/authorize", self.api_host);
|
||||||
|
let mut auth_url = match Url::parse(authz_endpoint.as_str()) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => err!(format!("Error parsing Duo authorization URL: {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut query_params = auth_url.query_pairs_mut();
|
||||||
|
query_params.append_pair("response_type", "code");
|
||||||
|
query_params.append_pair("client_id", self.client_id.as_str());
|
||||||
|
query_params.append_pair("request", token.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_auth_url = auth_url.to_string();
|
||||||
|
Ok(final_auth_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange the authorization code obtained from an access token provided by the user
|
||||||
|
// for the result of the MFA and validate.
|
||||||
|
// See: https://duo.com/docs/oauthapi#access-token (under Response Format)
|
||||||
|
async fn exchange_authz_code_for_result(
|
||||||
|
&self,
|
||||||
|
duo_code: &str,
|
||||||
|
duo_username: &str,
|
||||||
|
nonce: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if duo_code.is_empty() {
|
||||||
|
err!("Empty Duo authorization code")
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_url = format!("https://{}/oauth/v1/token", self.api_host);
|
||||||
|
|
||||||
|
let jwt_payload = self.new_client_assertion(&token_url);
|
||||||
|
|
||||||
|
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut post_body = HashMap::new();
|
||||||
|
post_body.insert("grant_type", String::from("authorization_code"));
|
||||||
|
post_body.insert("code", String::from(duo_code));
|
||||||
|
|
||||||
|
// Must be the same URL that was supplied in the authorization request for the supplied duo_code
|
||||||
|
post_body.insert("redirect_uri", self.redirect_uri.clone());
|
||||||
|
|
||||||
|
post_body
|
||||||
|
.insert("client_assertion_type", String::from("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"));
|
||||||
|
post_body.insert("client_assertion", token);
|
||||||
|
|
||||||
|
let res = match make_http_request(reqwest::Method::POST, &token_url)?
|
||||||
|
.header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)")
|
||||||
|
.form(&post_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Error exchanging Duo code: {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_code = res.status();
|
||||||
|
if status_code != StatusCode::OK {
|
||||||
|
err!(format!("Failure response from Duo: {}", status_code))
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: IdTokenResponse = match res.json::<IdTokenResponse>().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Error decoding ID token response: {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut validation = Validation::new(DUO_RESP_SIGNATURE_ALG);
|
||||||
|
validation.set_required_spec_claims(&["exp", "aud", "iss"]);
|
||||||
|
validation.set_audience(&[&self.client_id]);
|
||||||
|
validation.set_issuer(&[token_url.as_str()]);
|
||||||
|
|
||||||
|
let token_data = match jsonwebtoken::decode::<IdTokenClaims>(
|
||||||
|
&response.id_token,
|
||||||
|
&DecodingKey::from_secret(self.client_secret.as_bytes()),
|
||||||
|
&validation,
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => err!(format!("Failed to decode Duo token {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let matching_nonces = crypto::ct_eq(nonce, &token_data.claims.nonce);
|
||||||
|
let matching_usernames = crypto::ct_eq(duo_username, &token_data.claims.preferred_username);
|
||||||
|
|
||||||
|
if !(matching_nonces && matching_usernames) {
|
||||||
|
err!("Error validating Duo authorization, nonce or username mismatch.")
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DuoAuthContext {
|
||||||
|
pub state: String,
|
||||||
|
pub user_email: String,
|
||||||
|
pub nonce: String,
|
||||||
|
pub exp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a state string, retrieve the associated Duo auth context and
|
||||||
|
// delete the retrieved state from the database.
|
||||||
|
async fn extract_context(state: &str, conn: &mut DbConn) -> Option<DuoAuthContext> {
|
||||||
|
let ctx: TwoFactorDuoContext = match TwoFactorDuoContext::find_by_state(state, conn).await {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ctx.exp < Utc::now().timestamp() {
|
||||||
|
ctx.delete(conn).await.ok();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the context data, so that we can delete the context from
|
||||||
|
// the database before returning.
|
||||||
|
let ret_ctx = DuoAuthContext {
|
||||||
|
state: ctx.state.clone(),
|
||||||
|
user_email: ctx.user_email.clone(),
|
||||||
|
nonce: ctx.nonce.clone(),
|
||||||
|
exp: ctx.exp,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.delete(conn).await.ok();
|
||||||
|
Some(ret_ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task to clean up expired Duo authentication contexts that may have accumulated in the database.
|
||||||
|
pub async fn purge_duo_contexts(pool: DbPool) {
|
||||||
|
debug!("Purging Duo authentication contexts");
|
||||||
|
if let Ok(mut conn) = pool.get().await {
|
||||||
|
TwoFactorDuoContext::purge_expired_duo_contexts(&mut conn).await;
|
||||||
|
} else {
|
||||||
|
error!("Failed to get DB connection while purging expired Duo authentications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the url that Duo should redirect users to.
|
||||||
|
fn make_callback_url(client_name: &str) -> Result<String, Error> {
|
||||||
|
// Get the location of this application as defined in the config.
|
||||||
|
let base = match Url::parse(CONFIG.domain().as_str()) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => err!(format!("Error parsing configured domain URL (check your domain configuration): {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the client redirect bridge location
|
||||||
|
let mut callback = match base.join(DUO_REDIRECT_LOCATION) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => err!(format!("Error constructing Duo redirect URL (check your domain configuration): {e:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the 'client' string with the authenticating device type. The callback connector uses this
|
||||||
|
// information to figure out how it should handle certain clients.
|
||||||
|
{
|
||||||
|
let mut query_params = callback.query_pairs_mut();
|
||||||
|
query_params.append_pair("client", client_name);
|
||||||
|
}
|
||||||
|
Ok(callback.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-redirect first stage of the Duo OIDC authentication flow.
|
||||||
|
// Returns the "AuthUrl" that should be returned to clients for MFA.
|
||||||
|
pub async fn get_duo_auth_url(
|
||||||
|
email: &str,
|
||||||
|
client_id: &str,
|
||||||
|
device_identifier: &String,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||||
|
|
||||||
|
let callback_url = match make_callback_url(client_id) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = DuoClient::new(ik, sk, host, callback_url);
|
||||||
|
|
||||||
|
match client.health_check().await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random OAuth2 state and OIDC Nonce
|
||||||
|
let state: String = crypto::get_random_string_alphanum(STATE_LENGTH);
|
||||||
|
let nonce: String = crypto::get_random_string_alphanum(STATE_LENGTH);
|
||||||
|
|
||||||
|
// Bind the nonce to the device that's currently authing by hashing the nonce and device id
|
||||||
|
// and sending the result as the OIDC nonce.
|
||||||
|
let d: Digest = digest(&SHA512_256, format!("{nonce}{device_identifier}").as_bytes());
|
||||||
|
let hash: String = HEXLOWER.encode(d.as_ref());
|
||||||
|
|
||||||
|
match TwoFactorDuoContext::save(state.as_str(), email, nonce.as_str(), CTX_VALIDITY_SECS, conn).await {
|
||||||
|
Ok(()) => client.make_authz_req_url(email, state, hash),
|
||||||
|
Err(e) => err!(format!("Error saving Duo authentication context: {e:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-redirect second stage of the Duo OIDC authentication flow.
|
||||||
|
// Exchanges an authorization code for the MFA result with Duo's API and validates the result.
|
||||||
|
pub async fn validate_duo_login(
|
||||||
|
email: &str,
|
||||||
|
two_factor_token: &str,
|
||||||
|
client_id: &str,
|
||||||
|
device_identifier: &str,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
// Result supplied to us by clients in the form "<authz code>|<state>"
|
||||||
|
let split: Vec<&str> = two_factor_token.split('|').collect();
|
||||||
|
if split.len() != 2 {
|
||||||
|
err!(
|
||||||
|
"Invalid response length",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = split[0];
|
||||||
|
let state = split[1];
|
||||||
|
|
||||||
|
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||||
|
|
||||||
|
// Get the context by the state reported by the client. If we don't have one,
|
||||||
|
// it means the context is either missing or expired.
|
||||||
|
let ctx = match extract_context(state, conn).await {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
err!(
|
||||||
|
"Error validating duo authentication",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Context validation steps
|
||||||
|
let matching_usernames = crypto::ct_eq(email, &ctx.user_email);
|
||||||
|
|
||||||
|
// Probably redundant, but we're double-checking them anyway.
|
||||||
|
let matching_states = crypto::ct_eq(state, &ctx.state);
|
||||||
|
let unexpired_context = ctx.exp > Utc::now().timestamp();
|
||||||
|
|
||||||
|
if !(matching_usernames && matching_states && unexpired_context) {
|
||||||
|
err!(
|
||||||
|
"Error validating duo authentication",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback_url = match make_callback_url(client_id) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = DuoClient::new(ik, sk, host, callback_url);
|
||||||
|
|
||||||
|
match client.health_check().await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let d: Digest = digest(&SHA512_256, format!("{}{}", ctx.nonce, device_identifier).as_bytes());
|
||||||
|
let hash: String = HEXLOWER.encode(d.as_ref());
|
||||||
|
|
||||||
|
match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => {
|
||||||
|
err!(
|
||||||
|
"Error validating duo authentication",
|
||||||
|
ErrorEvent {
|
||||||
|
event: EventType::UserFailedLogIn2fa
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,10 @@ pub fn routes() -> Vec<Route> {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SendEmailLoginData {
|
struct SendEmailLoginData {
|
||||||
|
// DeviceIdentifier: String, // Currently not used
|
||||||
|
#[serde(alias = "Email")]
|
||||||
email: String,
|
email: String,
|
||||||
|
#[serde(alias = "MasterPasswordHash")]
|
||||||
master_password_hash: String,
|
master_password_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ use crate::{
|
|||||||
|
|
||||||
pub mod authenticator;
|
pub mod authenticator;
|
||||||
pub mod duo;
|
pub mod duo;
|
||||||
|
pub mod duo_oidc;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod protected_actions;
|
pub mod protected_actions;
|
||||||
pub mod webauthn;
|
pub mod webauthn;
|
||||||
@@ -268,10 +269,18 @@ pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
|||||||
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
"User {} did not complete a 2FA login within the configured time limit. IP: {}",
|
||||||
user.email, login.ip_address
|
user.email, login.ip_address
|
||||||
);
|
);
|
||||||
mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
|
match mail::send_incomplete_2fa_login(&user.email, &login.ip_address, &login.login_time, &login.device_name)
|
||||||
.await
|
.await
|
||||||
.expect("Error sending incomplete 2FA email");
|
{
|
||||||
login.delete(&mut conn).await.expect("Error deleting incomplete 2FA record");
|
Ok(_) => {
|
||||||
|
if let Err(e) = login.delete(&mut conn).await {
|
||||||
|
error!("Error deleting incomplete 2FA record: {e:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error sending incomplete 2FA email: {e:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
sync::{Arc, Mutex},
|
sync::Arc,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ use html5gum::{Emitter, HtmlString, InfallibleTokenizer, Readable, StringReader,
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
util::{get_reqwest_client_builder, Cached, CustomDnsResolver, CustomResolverError},
|
http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError},
|
||||||
|
util::Cached,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +54,6 @@ static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||||||
.timeout(icon_download_timeout)
|
.timeout(icon_download_timeout)
|
||||||
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
.pool_max_idle_per_host(5) // Configure the Hyper Pool to only have max 5 idle connections
|
||||||
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
.pool_idle_timeout(pool_idle_timeout) // Configure the Hyper Pool to timeout after 10 seconds
|
||||||
.dns_resolver(CustomDnsResolver::instance())
|
|
||||||
.default_headers(default_headers.clone())
|
.default_headers(default_headers.clone())
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build client")
|
.expect("Failed to build client")
|
||||||
@@ -69,7 +69,8 @@ fn icon_external(domain: &str) -> Option<Redirect> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_domain_blacklisted(domain) {
|
if should_block_address(domain) {
|
||||||
|
warn!("Blocked address: {}", domain);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +100,15 @@ async fn icon_internal(domain: &str) -> Cached<(ContentType, Vec<u8>)> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if should_block_address(domain) {
|
||||||
|
warn!("Blocked address: {}", domain);
|
||||||
|
return Cached::ttl(
|
||||||
|
(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()),
|
||||||
|
CONFIG.icon_cache_negttl(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
match get_icon(domain).await {
|
match get_icon(domain).await {
|
||||||
Some((icon, icon_type)) => {
|
Some((icon, icon_type)) => {
|
||||||
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
Cached::ttl((ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl(), true)
|
||||||
@@ -144,30 +154,6 @@ fn is_valid_domain(domain: &str) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_domain_blacklisted(domain: &str) -> bool {
|
|
||||||
let Some(config_blacklist) = CONFIG.icon_blacklist_regex() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compiled domain blacklist
|
|
||||||
static COMPILED_BLACKLIST: Mutex<Option<(String, Regex)>> = Mutex::new(None);
|
|
||||||
let mut guard = COMPILED_BLACKLIST.lock().unwrap();
|
|
||||||
|
|
||||||
// If the stored regex is up to date, use it
|
|
||||||
if let Some((value, regex)) = &*guard {
|
|
||||||
if value == &config_blacklist {
|
|
||||||
return regex.is_match(domain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have a regex stored, or it's not up to date, recreate it
|
|
||||||
let regex = Regex::new(&config_blacklist).unwrap();
|
|
||||||
let is_match = regex.is_match(domain);
|
|
||||||
*guard = Some((config_blacklist, regex));
|
|
||||||
|
|
||||||
is_match
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||||
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain);
|
||||||
|
|
||||||
@@ -195,9 +181,9 @@ async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
|||||||
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string()))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// If this error comes from the custom resolver, this means this is a blacklisted domain
|
// If this error comes from the custom resolver, this means this is a blocked domain
|
||||||
// or non global IP, don't save the miss file in this case to avoid leaking it
|
// or non global IP, don't save the miss file in this case to avoid leaking it
|
||||||
if let Some(error) = CustomResolverError::downcast_ref(&e) {
|
if let Some(error) = CustomHttpClientError::downcast_ref(&e) {
|
||||||
warn!("{error}");
|
warn!("{error}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -353,7 +339,7 @@ async fn get_icon_url(domain: &str) -> Result<IconUrlResult, Error> {
|
|||||||
|
|
||||||
// First check the domain as given during the request for HTTPS.
|
// First check the domain as given during the request for HTTPS.
|
||||||
let resp = match get_page(&ssldomain).await {
|
let resp = match get_page(&ssldomain).await {
|
||||||
Err(e) if CustomResolverError::downcast_ref(&e).is_none() => {
|
Err(e) if CustomHttpClientError::downcast_ref(&e).is_none() => {
|
||||||
// If we get an error that is not caused by the blacklist, we retry with HTTP
|
// If we get an error that is not caused by the blacklist, we retry with HTTP
|
||||||
match get_page(&httpdomain).await {
|
match get_page(&httpdomain).await {
|
||||||
mut sub_resp @ Err(_) => {
|
mut sub_resp @ Err(_) => {
|
||||||
|
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
core::{
|
core::{
|
||||||
accounts::{PreloginData, RegisterData, _prelogin, _register},
|
accounts::{PreloginData, RegisterData, _prelogin, _register},
|
||||||
log_user_event,
|
log_user_event,
|
||||||
two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey},
|
two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},
|
||||||
},
|
},
|
||||||
push::register_push_device,
|
push::register_push_device,
|
||||||
ApiResult, EmptyResult, JsonResult,
|
ApiResult, EmptyResult, JsonResult,
|
||||||
@@ -502,7 +502,9 @@ async 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).await?, "2FA token not provided"),
|
None => {
|
||||||
|
err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA token not provided")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
@@ -519,7 +521,23 @@ async fn twofactor_auth(
|
|||||||
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
||||||
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||||
Some(TwoFactorType::Duo) => {
|
Some(TwoFactorType::Duo) => {
|
||||||
duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?
|
match CONFIG.duo_use_iframe() {
|
||||||
|
true => {
|
||||||
|
// Legacy iframe prompt flow
|
||||||
|
duo::validate_duo_login(&user.email, twofactor_code, conn).await?
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
// OIDC based flow
|
||||||
|
duo_oidc::validate_duo_login(
|
||||||
|
&user.email,
|
||||||
|
twofactor_code,
|
||||||
|
data.client_id.as_ref().unwrap(),
|
||||||
|
data.device_identifier.as_ref().unwrap(),
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(TwoFactorType::Email) => {
|
Some(TwoFactorType::Email) => {
|
||||||
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await?
|
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await?
|
||||||
@@ -532,7 +550,7 @@ async fn twofactor_auth(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?,
|
||||||
"2FA Remember token not provided"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -560,7 +578,12 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
|||||||
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult<Value> {
|
async fn _json_err_twofactor(
|
||||||
|
providers: &[i32],
|
||||||
|
user_uuid: &str,
|
||||||
|
data: &ConnectData,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"error" : "invalid_grant",
|
"error" : "invalid_grant",
|
||||||
"error_description" : "Two factor required.",
|
"error_description" : "Two factor required.",
|
||||||
@@ -588,12 +611,30 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo
|
|||||||
None => err!("User does not exist"),
|
None => err!("User does not exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
match CONFIG.duo_use_iframe() {
|
||||||
|
true => {
|
||||||
|
// Legacy iframe prompt flow
|
||||||
|
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
||||||
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
|
"Host": host,
|
||||||
|
"Signature": signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
// OIDC based flow
|
||||||
|
let auth_url = duo_oidc::get_duo_auth_url(
|
||||||
|
&email,
|
||||||
|
data.client_id.as_ref().unwrap(),
|
||||||
|
data.device_identifier.as_ref().unwrap(),
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
"Host": host,
|
"AuthUrl": auth_url,
|
||||||
"Signature": signature,
|
})
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||||
|
@@ -20,7 +20,7 @@ pub use crate::api::{
|
|||||||
core::two_factor::send_incomplete_2fa_notifications,
|
core::two_factor::send_incomplete_2fa_notifications,
|
||||||
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
||||||
core::{event_cleanup_job, events_routes as core_events_routes},
|
core::{event_cleanup_job, events_routes as core_events_routes},
|
||||||
icons::{is_domain_blacklisted, routes as icons_routes},
|
icons::routes as icons_routes,
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
notifications::routes as notifications_routes,
|
notifications::routes as notifications_routes,
|
||||||
notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},
|
notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
use reqwest::{
|
||||||
|
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
|
||||||
|
Method,
|
||||||
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{ApiResult, EmptyResult, UpdateType},
|
api::{ApiResult, EmptyResult, UpdateType},
|
||||||
db::models::{Cipher, Device, Folder, Send, User},
|
db::models::{Cipher, Device, Folder, Send, User},
|
||||||
util::get_reqwest_client,
|
http_client::make_http_request,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,8 +53,7 @@ async fn get_auth_push_token() -> ApiResult<String> {
|
|||||||
("client_secret", &client_secret),
|
("client_secret", &client_secret),
|
||||||
];
|
];
|
||||||
|
|
||||||
let res = match get_reqwest_client()
|
let res = match make_http_request(Method::POST, &format!("{}/connect/token", CONFIG.push_identity_uri()))?
|
||||||
.post(&format!("{}/connect/token", CONFIG.push_identity_uri()))
|
|
||||||
.form(¶ms)
|
.form(¶ms)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -104,8 +106,7 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
|
|||||||
let auth_push_token = get_auth_push_token().await?;
|
let auth_push_token = get_auth_push_token().await?;
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
if let Err(e) = get_reqwest_client()
|
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))?
|
||||||
.post(CONFIG.push_relay_uri() + "/push/register")
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.header(ACCEPT, "application/json")
|
.header(ACCEPT, "application/json")
|
||||||
.header(AUTHORIZATION, auth_header)
|
.header(AUTHORIZATION, auth_header)
|
||||||
@@ -132,8 +133,7 @@ pub async fn unregister_push_device(push_uuid: Option<String>) -> EmptyResult {
|
|||||||
|
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
match get_reqwest_client()
|
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()))?
|
||||||
.delete(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap())
|
|
||||||
.header(AUTHORIZATION, auth_header)
|
.header(AUTHORIZATION, auth_header)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -266,8 +266,15 @@ async fn send_to_push_relay(notification_data: Value) {
|
|||||||
|
|
||||||
let auth_header = format!("Bearer {}", &auth_push_token);
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
if let Err(e) = get_reqwest_client()
|
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
||||||
.post(CONFIG.push_relay_uri() + "/push/send")
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!("An error occurred while sending a send update to the push relay: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = req
|
||||||
.header(ACCEPT, "application/json")
|
.header(ACCEPT, "application/json")
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.header(AUTHORIZATION, &auth_header)
|
.header(AUTHORIZATION, &auth_header)
|
||||||
|
72
src/auth.rs
72
src/auth.rs
@@ -1,13 +1,18 @@
|
|||||||
// JWT Handling
|
// JWT Handling
|
||||||
//
|
//
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
|
use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
|
|
||||||
use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
|
|
||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
net::IpAddr,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{error::Error, CONFIG};
|
use crate::{error::Error, CONFIG};
|
||||||
|
|
||||||
@@ -31,27 +36,36 @@ static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
|||||||
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
||||||
|
|
||||||
pub fn initialize_keys() -> Result<(), crate::error::Error> {
|
pub fn initialize_keys() -> Result<(), crate::error::Error> {
|
||||||
let mut priv_key_buffer = Vec::with_capacity(2048);
|
fn read_key(create_if_missing: bool) -> Result<(Rsa<openssl::pkey::Private>, Vec<u8>), crate::error::Error> {
|
||||||
|
let mut priv_key_buffer = Vec::with_capacity(2048);
|
||||||
|
|
||||||
let priv_key = {
|
let mut priv_key_file = File::options()
|
||||||
let mut priv_key_file =
|
.create(create_if_missing)
|
||||||
File::options().create(true).truncate(false).read(true).write(true).open(CONFIG.private_rsa_key())?;
|
.truncate(false)
|
||||||
|
.read(true)
|
||||||
|
.write(create_if_missing)
|
||||||
|
.open(CONFIG.private_rsa_key())?;
|
||||||
|
|
||||||
#[allow(clippy::verbose_file_reads)]
|
#[allow(clippy::verbose_file_reads)]
|
||||||
let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?;
|
let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?;
|
||||||
|
|
||||||
if bytes_read > 0 {
|
let rsa_key = if bytes_read > 0 {
|
||||||
Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])?
|
Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])?
|
||||||
} else {
|
} else if create_if_missing {
|
||||||
// Only create the key if the file doesn't exist or is empty
|
// Only create the key if the file doesn't exist or is empty
|
||||||
let rsa_key = openssl::rsa::Rsa::generate(2048)?;
|
let rsa_key = openssl::rsa::Rsa::generate(2048)?;
|
||||||
priv_key_buffer = rsa_key.private_key_to_pem()?;
|
priv_key_buffer = rsa_key.private_key_to_pem()?;
|
||||||
priv_key_file.write_all(&priv_key_buffer)?;
|
priv_key_file.write_all(&priv_key_buffer)?;
|
||||||
info!("Private key created correctly.");
|
info!("Private key '{}' created correctly", CONFIG.private_rsa_key());
|
||||||
rsa_key
|
rsa_key
|
||||||
}
|
} else {
|
||||||
};
|
err!("Private key does not exist or invalid format", CONFIG.private_rsa_key());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((rsa_key, priv_key_buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
let (priv_key, priv_key_buffer) = read_key(true).or_else(|_| read_key(false))?;
|
||||||
let pub_key_buffer = priv_key.public_key_to_pem()?;
|
let pub_key_buffer = priv_key.public_key_to_pem()?;
|
||||||
|
|
||||||
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
|
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
|
||||||
@@ -379,8 +393,6 @@ impl<'r> FromRequest<'r> for Host {
|
|||||||
referer.to_string()
|
referer.to_string()
|
||||||
} else {
|
} else {
|
||||||
// Try to guess from the headers
|
// Try to guess from the headers
|
||||||
use std::env;
|
|
||||||
|
|
||||||
let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") {
|
let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") {
|
||||||
proto
|
proto
|
||||||
} else if env::var("ROCKET_TLS").is_ok() {
|
} else if env::var("ROCKET_TLS").is_ok() {
|
||||||
@@ -805,11 +817,6 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
|||||||
//
|
//
|
||||||
// Client IP address detection
|
// Client IP address detection
|
||||||
//
|
//
|
||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
net::IpAddr,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ClientIp {
|
pub struct ClientIp {
|
||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
@@ -842,6 +849,35 @@ impl<'r> FromRequest<'r> for ClientIp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Secure {
|
||||||
|
pub https: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Secure {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let headers = request.headers();
|
||||||
|
|
||||||
|
// Try to guess from the headers
|
||||||
|
let protocol = match headers.get_one("X-Forwarded-Proto") {
|
||||||
|
Some(proto) => proto,
|
||||||
|
None => {
|
||||||
|
if env::var("ROCKET_TLS").is_ok() {
|
||||||
|
"https"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Outcome::Success(Secure {
|
||||||
|
https: protocol == "https",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WsAccessTokenHeader {
|
pub struct WsAccessTokenHeader {
|
||||||
pub access_token: Option<String>,
|
pub access_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
@@ -146,6 +146,12 @@ macro_rules! make_config {
|
|||||||
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
||||||
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
|
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
|
||||||
|
|
||||||
|
|
||||||
|
// Copy the values from the deprecated flags to the new ones
|
||||||
|
if config.http_request_block_regex.is_none() {
|
||||||
|
config.http_request_block_regex = config.icon_blacklist_regex.clone();
|
||||||
|
}
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +415,9 @@ make_config! {
|
|||||||
/// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request.
|
/// Auth Request cleanup schedule |> Cron schedule of the job that cleans old auth requests from the auth request.
|
||||||
/// Defaults to every minute. Set blank to disable this job.
|
/// Defaults to every minute. Set blank to disable this job.
|
||||||
auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string();
|
auth_request_purge_schedule: String, false, def, "30 * * * * *".to_string();
|
||||||
|
/// Duo Auth context cleanup schedule |> Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt.
|
||||||
|
/// Defaults to once every minute. Set blank to disable this job.
|
||||||
|
duo_context_purge_schedule: String, false, def, "30 * * * * *".to_string();
|
||||||
},
|
},
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
@@ -531,12 +539,18 @@ make_config! {
|
|||||||
icon_cache_negttl: u64, true, def, 259_200;
|
icon_cache_negttl: u64, true, def, 259_200;
|
||||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||||
icon_download_timeout: u64, true, def, 10;
|
icon_download_timeout: u64, true, def, 10;
|
||||||
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
|
|
||||||
|
/// [Deprecated] Icon blacklist Regex |> Use `http_request_block_regex` instead
|
||||||
|
icon_blacklist_regex: String, false, option;
|
||||||
|
/// [Deprecated] Icon blacklist non global IPs |> Use `http_request_block_non_global_ips` instead
|
||||||
|
icon_blacklist_non_global_ips: bool, false, def, true;
|
||||||
|
|
||||||
|
/// Block HTTP domains/IPs by Regex |> Any domains or IPs that match this regex won't be fetched by the internal HTTP client.
|
||||||
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
/// Useful to hide other servers in the local network. Check the WIKI for more details
|
||||||
icon_blacklist_regex: String, true, option;
|
http_request_block_regex: String, true, option;
|
||||||
/// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
|
/// Block non global IPs |> Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address.
|
||||||
/// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
/// Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
|
||||||
icon_blacklist_non_global_ips: bool, true, def, true;
|
http_request_block_non_global_ips: bool, true, auto, |c| c.icon_blacklist_non_global_ips;
|
||||||
|
|
||||||
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
||||||
/// Note that the checkbox would still be present, but ignored.
|
/// Note that the checkbox would still be present, but ignored.
|
||||||
@@ -564,8 +578,9 @@ make_config! {
|
|||||||
use_syslog: bool, false, def, false;
|
use_syslog: bool, false, def, false;
|
||||||
/// Log file path
|
/// Log file path
|
||||||
log_file: String, false, option;
|
log_file: String, false, option;
|
||||||
/// Log level
|
/// Log level |> Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
||||||
log_level: String, false, def, "Info".to_string();
|
/// For a specific module append it as a comma separated value "info,path::to::module=debug"
|
||||||
|
log_level: String, false, def, "info".to_string();
|
||||||
|
|
||||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems,
|
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using vaultwarden on some exotic filesystems,
|
||||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||||
@@ -603,7 +618,13 @@ make_config! {
|
|||||||
admin_session_lifetime: i64, true, def, 20;
|
admin_session_lifetime: i64, true, def, 20;
|
||||||
|
|
||||||
/// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).
|
/// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).
|
||||||
org_groups_enabled: bool, false, def, false;
|
org_groups_enabled: bool, false, def, false;
|
||||||
|
|
||||||
|
/// Increase note size limit (Know the risks!) |> Sets the secure note size limit to 100_000 instead of the default 10_000.
|
||||||
|
/// WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers!
|
||||||
|
increase_note_size_limit: bool, true, def, false;
|
||||||
|
/// Generated max_note_size value to prevent if..else matching during every check
|
||||||
|
_max_note_size: usize, false, gen, |c| if c.increase_note_size_limit {100_000} else {10_000};
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Yubikey settings
|
/// Yubikey settings
|
||||||
@@ -622,6 +643,8 @@ make_config! {
|
|||||||
duo: _enable_duo {
|
duo: _enable_duo {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
_enable_duo: bool, true, def, true;
|
_enable_duo: bool, true, def, true;
|
||||||
|
/// Attempt to use deprecated iframe-based Traditional Prompt (Duo WebSDK 2)
|
||||||
|
duo_use_iframe: bool, false, def, false;
|
||||||
/// Integration Key
|
/// Integration Key
|
||||||
duo_ikey: String, true, option;
|
duo_ikey: String, true, option;
|
||||||
/// Secret Key
|
/// Secret Key
|
||||||
@@ -899,12 +922,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!("To use email 2FA as automatic fallback, email 2fa has to be enabled!");
|
err!("To use email 2FA as automatic fallback, email 2fa has to be enabled!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the icon blacklist regex is valid
|
// Check if the HTTP request block regex is valid
|
||||||
if let Some(ref r) = cfg.icon_blacklist_regex {
|
if let Some(ref r) = cfg.http_request_block_regex {
|
||||||
let validate_regex = regex::Regex::new(r);
|
let validate_regex = regex::Regex::new(r);
|
||||||
match validate_regex {
|
match validate_regex {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => err!(format!("`ICON_BLACKLIST_REGEX` is invalid: {e:#?}")),
|
Err(e) => err!(format!("`HTTP_REQUEST_BLOCK_REGEX` is invalid: {e:#?}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,6 +1007,11 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.increase_note_size_limit {
|
||||||
|
println!("[WARNING] Secure Note size limit is increased to 100_000!");
|
||||||
|
println!("[WARNING] This could cause issues with clients. Also exports will not work on Bitwarden servers!.");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,7 +1293,6 @@ where
|
|||||||
hb.set_strict_mode(true);
|
hb.set_strict_mode(true);
|
||||||
// Register helpers
|
// Register helpers
|
||||||
hb.register_helper("case", Box::new(case_helper));
|
hb.register_helper("case", Box::new(case_helper));
|
||||||
hb.register_helper("jsesc", Box::new(js_escape_helper));
|
|
||||||
hb.register_helper("to_json", Box::new(to_json));
|
hb.register_helper("to_json", Box::new(to_json));
|
||||||
|
|
||||||
macro_rules! reg {
|
macro_rules! reg {
|
||||||
@@ -1323,14 +1350,7 @@ where
|
|||||||
// And then load user templates to overwrite the defaults
|
// And then load user templates to overwrite the defaults
|
||||||
// Use .hbs extension for the files
|
// Use .hbs extension for the files
|
||||||
// Templates get registered with their relative name
|
// Templates get registered with their relative name
|
||||||
hb.register_templates_directory(
|
hb.register_templates_directory(path, DirectorySourceOptions::default()).unwrap();
|
||||||
path,
|
|
||||||
DirectorySourceOptions {
|
|
||||||
tpl_extension: ".hbs".to_owned(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
hb
|
hb
|
||||||
}
|
}
|
||||||
@@ -1353,32 +1373,6 @@ fn case_helper<'reg, 'rc>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn js_escape_helper<'reg, 'rc>(
|
|
||||||
h: &Helper<'rc>,
|
|
||||||
_r: &'reg Handlebars<'_>,
|
|
||||||
_ctx: &'rc Context,
|
|
||||||
_rc: &mut RenderContext<'reg, 'rc>,
|
|
||||||
out: &mut dyn Output,
|
|
||||||
) -> HelperResult {
|
|
||||||
let param =
|
|
||||||
h.param(0).ok_or_else(|| RenderErrorReason::Other(String::from("Param not found for helper \"jsesc\"")))?;
|
|
||||||
|
|
||||||
let no_quote = h.param(1).is_some();
|
|
||||||
|
|
||||||
let value = param
|
|
||||||
.value()
|
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| RenderErrorReason::Other(String::from("Param for helper \"jsesc\" is not a String")))?;
|
|
||||||
|
|
||||||
let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
|
|
||||||
if !no_quote {
|
|
||||||
escaped_value = format!(""{escaped_value}"");
|
|
||||||
}
|
|
||||||
|
|
||||||
out.write(&escaped_value)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_json<'reg, 'rc>(
|
fn to_json<'reg, 'rc>(
|
||||||
h: &Helper<'rc>,
|
h: &Helper<'rc>,
|
||||||
_r: &'reg Handlebars<'_>,
|
_r: &'reg Handlebars<'_>,
|
||||||
|
@@ -81,16 +81,14 @@ impl Cipher {
|
|||||||
|
|
||||||
pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult {
|
pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult {
|
||||||
let mut validation_errors = serde_json::Map::new();
|
let mut validation_errors = serde_json::Map::new();
|
||||||
|
let max_note_size = CONFIG._max_note_size();
|
||||||
|
let max_note_size_msg =
|
||||||
|
format!("The field Notes exceeds the maximum encrypted value length of {} characters.", &max_note_size);
|
||||||
for (index, cipher) in cipher_data.iter().enumerate() {
|
for (index, cipher) in cipher_data.iter().enumerate() {
|
||||||
if let Some(note) = &cipher.notes {
|
if let Some(note) = &cipher.notes {
|
||||||
if note.len() > 10_000 {
|
if note.len() > max_note_size {
|
||||||
validation_errors.insert(
|
validation_errors
|
||||||
format!("Ciphers[{index}].Notes"),
|
.insert(format!("Ciphers[{index}].Notes"), serde_json::to_value([&max_note_size_msg]).unwrap());
|
||||||
serde_json::to_value([
|
|
||||||
"The field Notes exceeds the maximum encrypted value length of 10000 characters.",
|
|
||||||
])
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +187,11 @@ impl Cipher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix secure note issues when data is `{}`
|
// Fix secure note issues when data is invalid
|
||||||
// This breaks at least the native mobile clients
|
// This breaks at least the native mobile clients
|
||||||
if self.atype == 2 && (self.data.eq("{}") || self.data.to_ascii_lowercase().eq("{\"type\":null}")) {
|
if self.atype == 2
|
||||||
|
&& (self.data.is_empty() || self.data.eq("{}") || self.data.to_ascii_lowercase().eq("{\"type\":null}"))
|
||||||
|
{
|
||||||
type_data_json = json!({"type": 0});
|
type_data_json = json!({"type": 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +620,17 @@ impl Cipher {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
ciphers::table
|
||||||
|
.filter(ciphers::uuid.eq(cipher_uuid))
|
||||||
|
.filter(ciphers::organization_uuid.eq(org_uuid))
|
||||||
|
.first::<CipherDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
// Find all ciphers accessible or visible to the specified user.
|
// Find all ciphers accessible or visible to the specified user.
|
||||||
//
|
//
|
||||||
// "Accessible" means the user has read access to the cipher, either via
|
// "Accessible" means the user has read access to the cipher, either via
|
||||||
|
@@ -12,6 +12,7 @@ mod org_policy;
|
|||||||
mod organization;
|
mod organization;
|
||||||
mod send;
|
mod send;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
|
mod two_factor_duo_context;
|
||||||
mod two_factor_incomplete;
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
@@ -29,5 +30,6 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
|||||||
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
pub use self::send::{Send, SendType};
|
pub use self::send::{Send, SendType};
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
|
pub use self::two_factor_duo_context::TwoFactorDuoContext;
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
|
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
|
||||||
|
@@ -156,11 +156,12 @@ impl Organization {
|
|||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
"identifier": null, // not supported by us
|
"identifier": null, // not supported by us
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"seats": 10, // The value doesn't matter, we don't check server-side
|
"seats": null,
|
||||||
// "maxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side
|
"maxAutoscaleSeats": null,
|
||||||
"maxCollections": 10, // The value doesn't matter, we don't check server-side
|
"maxCollections": null,
|
||||||
"maxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side
|
||||||
"use2fa": true,
|
"use2fa": true,
|
||||||
|
"useCustomPermissions": false,
|
||||||
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||||
"useEvents": CONFIG.org_events_enabled(),
|
"useEvents": CONFIG.org_events_enabled(),
|
||||||
"useGroups": CONFIG.org_groups_enabled(),
|
"useGroups": CONFIG.org_groups_enabled(),
|
||||||
@@ -182,8 +183,7 @@ impl Organization {
|
|||||||
"businessTaxNumber": null,
|
"businessTaxNumber": null,
|
||||||
|
|
||||||
"billingEmail": self.billing_email,
|
"billingEmail": self.billing_email,
|
||||||
"plan": "TeamsAnnually",
|
"planType": 6, // Custom plan
|
||||||
"planType": 5, // TeamsAnnually plan
|
|
||||||
"usersGetPremium": true,
|
"usersGetPremium": true,
|
||||||
"object": "organization",
|
"object": "organization",
|
||||||
})
|
})
|
||||||
@@ -369,8 +369,9 @@ impl UserOrganization {
|
|||||||
"id": self.org_uuid,
|
"id": self.org_uuid,
|
||||||
"identifier": null, // Not supported
|
"identifier": null, // Not supported
|
||||||
"name": org.name,
|
"name": org.name,
|
||||||
"seats": 10, // The value doesn't matter, we don't check server-side
|
"seats": null,
|
||||||
"maxCollections": 10, // The value doesn't matter, we don't check server-side
|
"maxAutoscaleSeats": null,
|
||||||
|
"maxCollections": null,
|
||||||
"usersGetPremium": true,
|
"usersGetPremium": true,
|
||||||
"use2fa": true,
|
"use2fa": true,
|
||||||
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
"useDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
|
||||||
@@ -392,12 +393,14 @@ impl UserOrganization {
|
|||||||
"useCustomPermissions": false,
|
"useCustomPermissions": false,
|
||||||
"useActivateAutofillPolicy": false,
|
"useActivateAutofillPolicy": false,
|
||||||
|
|
||||||
|
"organizationUserId": self.uuid,
|
||||||
"providerId": null,
|
"providerId": null,
|
||||||
"providerName": null,
|
"providerName": null,
|
||||||
"providerType": null,
|
"providerType": null,
|
||||||
"familySponsorshipFriendlyName": null,
|
"familySponsorshipFriendlyName": null,
|
||||||
"familySponsorshipAvailable": false,
|
"familySponsorshipAvailable": false,
|
||||||
"planProductType": 0,
|
"planProductType": 3,
|
||||||
|
"productTierType": 3, // Enterprise tier
|
||||||
"keyConnectorEnabled": false,
|
"keyConnectorEnabled": false,
|
||||||
"keyConnectorUrl": null,
|
"keyConnectorUrl": null,
|
||||||
"familySponsorshipLastSyncDate": null,
|
"familySponsorshipLastSyncDate": null,
|
||||||
@@ -410,7 +413,7 @@ impl UserOrganization {
|
|||||||
|
|
||||||
"permissions": permissions,
|
"permissions": permissions,
|
||||||
|
|
||||||
"maxStorageGb": 10, // The value doesn't matter, we don't check server-side
|
"maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side
|
||||||
|
|
||||||
// These are per user
|
// These are per user
|
||||||
"userId": self.user_uuid,
|
"userId": self.user_uuid,
|
||||||
@@ -734,6 +737,19 @@ impl UserOrganization {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_confirmed_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
users_organizations::table
|
||||||
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
|
.filter(
|
||||||
|
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
|
||||||
|
)
|
||||||
|
.first::<UserOrganizationDb>(conn)
|
||||||
|
.ok().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
|
84
src/db/models/two_factor_duo_context.rs
Normal file
84
src/db/models/two_factor_duo_context.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
||||||
|
#[diesel(table_name = twofactor_duo_ctx)]
|
||||||
|
#[diesel(primary_key(state))]
|
||||||
|
pub struct TwoFactorDuoContext {
|
||||||
|
pub state: String,
|
||||||
|
pub user_email: String,
|
||||||
|
pub nonce: String,
|
||||||
|
pub exp: i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TwoFactorDuoContext {
|
||||||
|
pub async fn find_by_state(state: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! {
|
||||||
|
conn: {
|
||||||
|
twofactor_duo_ctx::table
|
||||||
|
.filter(twofactor_duo_ctx::state.eq(state))
|
||||||
|
.first::<TwoFactorDuoContextDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(state: &str, user_email: &str, nonce: &str, ttl: i64, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
// A saved context should never be changed, only created or deleted.
|
||||||
|
let exists = Self::find_by_state(state, conn).await;
|
||||||
|
if exists.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let exp = Utc::now().timestamp() + ttl;
|
||||||
|
|
||||||
|
db_run! {
|
||||||
|
conn: {
|
||||||
|
diesel::insert_into(twofactor_duo_ctx::table)
|
||||||
|
.values((
|
||||||
|
twofactor_duo_ctx::state.eq(state),
|
||||||
|
twofactor_duo_ctx::user_email.eq(user_email),
|
||||||
|
twofactor_duo_ctx::nonce.eq(nonce),
|
||||||
|
twofactor_duo_ctx::exp.eq(exp)
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving context to twofactor_duo_ctx")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_expired(conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
db_run! {
|
||||||
|
conn: {
|
||||||
|
twofactor_duo_ctx::table
|
||||||
|
.filter(twofactor_duo_ctx::exp.lt(now))
|
||||||
|
.load::<TwoFactorDuoContextDb>(conn)
|
||||||
|
.expect("Error finding expired contexts in twofactor_duo_ctx")
|
||||||
|
.from_db()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! {
|
||||||
|
conn: {
|
||||||
|
diesel::delete(
|
||||||
|
twofactor_duo_ctx::table
|
||||||
|
.filter(twofactor_duo_ctx::state.eq(&self.state)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting from twofactor_duo_ctx")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn purge_expired_duo_contexts(conn: &mut DbConn) {
|
||||||
|
for context in Self::find_expired(conn).await {
|
||||||
|
context.delete(conn).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -174,6 +174,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_duo_ctx (state) {
|
||||||
|
state -> Text,
|
||||||
|
user_email -> Text,
|
||||||
|
nonce -> Text,
|
||||||
|
exp -> BigInt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -174,6 +174,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_duo_ctx (state) {
|
||||||
|
state -> Text,
|
||||||
|
user_email -> Text,
|
||||||
|
nonce -> Text,
|
||||||
|
exp -> BigInt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -174,6 +174,15 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
twofactor_duo_ctx (state) {
|
||||||
|
state -> Text,
|
||||||
|
user_email -> Text,
|
||||||
|
nonce -> Text,
|
||||||
|
exp -> BigInt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users (uuid) {
|
users (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
// Error generator macro
|
// Error generator macro
|
||||||
//
|
//
|
||||||
use crate::db::models::EventType;
|
use crate::db::models::EventType;
|
||||||
|
use crate::http_client::CustomHttpClientError;
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
|
|
||||||
macro_rules! make_error {
|
macro_rules! make_error {
|
||||||
@@ -68,6 +69,10 @@ make_error! {
|
|||||||
Empty(Empty): _no_source, _serialize,
|
Empty(Empty): _no_source, _serialize,
|
||||||
// Used to represent err! calls
|
// Used to represent err! calls
|
||||||
Simple(String): _no_source, _api_error,
|
Simple(String): _no_source, _api_error,
|
||||||
|
|
||||||
|
// Used in our custom http client to handle non-global IPs and blocked domains
|
||||||
|
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
|
||||||
|
|
||||||
// Used for special return values, like 2FA errors
|
// Used for special return values, like 2FA errors
|
||||||
Json(Value): _no_source, _serialize,
|
Json(Value): _no_source, _serialize,
|
||||||
Db(DieselErr): _has_source, _api_error,
|
Db(DieselErr): _has_source, _api_error,
|
||||||
|
246
src/http_client.rs
Normal file
246
src/http_client.rs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::{
|
||||||
|
dns::{Name, Resolve, Resolving},
|
||||||
|
header, Client, ClientBuilder,
|
||||||
|
};
|
||||||
|
use url::Host;
|
||||||
|
|
||||||
|
use crate::{util::is_global, CONFIG};
|
||||||
|
|
||||||
|
pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> {
|
||||||
|
let Ok(url) = url::Url::parse(url) else {
|
||||||
|
err!("Invalid URL");
|
||||||
|
};
|
||||||
|
let Some(host) = url.host() else {
|
||||||
|
err!("Invalid host");
|
||||||
|
};
|
||||||
|
|
||||||
|
should_block_host(host)?;
|
||||||
|
|
||||||
|
static INSTANCE: Lazy<Client> = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client"));
|
||||||
|
|
||||||
|
Ok(INSTANCE.request(method, url))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_reqwest_client_builder() -> ClientBuilder {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden"));
|
||||||
|
|
||||||
|
let redirect_policy = reqwest::redirect::Policy::custom(|attempt| {
|
||||||
|
if attempt.previous().len() >= 5 {
|
||||||
|
return attempt.error("Too many redirects");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(host) = attempt.url().host() else {
|
||||||
|
return attempt.error("Invalid host");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = should_block_host(host) {
|
||||||
|
return attempt.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt.follow()
|
||||||
|
});
|
||||||
|
|
||||||
|
Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.redirect(redirect_policy)
|
||||||
|
.dns_resolver(CustomDnsResolver::instance())
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_block_address(domain_or_ip: &str) -> bool {
|
||||||
|
if let Ok(ip) = IpAddr::from_str(domain_or_ip) {
|
||||||
|
if should_block_ip(ip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
should_block_address_regex(domain_or_ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_block_ip(ip: IpAddr) -> bool {
|
||||||
|
if !CONFIG.http_request_block_non_global_ips() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
!is_global(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_block_address_regex(domain_or_ip: &str) -> bool {
|
||||||
|
let Some(block_regex) = CONFIG.http_request_block_regex() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static COMPILED_REGEX: Mutex<Option<(String, Regex)>> = Mutex::new(None);
|
||||||
|
let mut guard = COMPILED_REGEX.lock().unwrap();
|
||||||
|
|
||||||
|
// If the stored regex is up to date, use it
|
||||||
|
if let Some((value, regex)) = &*guard {
|
||||||
|
if value == &block_regex {
|
||||||
|
return regex.is_match(domain_or_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a regex stored, or it's not up to date, recreate it
|
||||||
|
let regex = Regex::new(&block_regex).unwrap();
|
||||||
|
let is_match = regex.is_match(domain_or_ip);
|
||||||
|
*guard = Some((block_regex, regex));
|
||||||
|
|
||||||
|
is_match
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_block_host(host: Host<&str>) -> Result<(), CustomHttpClientError> {
|
||||||
|
let (ip, host_str): (Option<IpAddr>, String) = match host {
|
||||||
|
url::Host::Ipv4(ip) => (Some(ip.into()), ip.to_string()),
|
||||||
|
url::Host::Ipv6(ip) => (Some(ip.into()), ip.to_string()),
|
||||||
|
url::Host::Domain(d) => (None, d.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
if should_block_ip(ip) {
|
||||||
|
return Err(CustomHttpClientError::NonGlobalIp {
|
||||||
|
domain: None,
|
||||||
|
ip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_block_address_regex(&host_str) {
|
||||||
|
return Err(CustomHttpClientError::Blocked {
|
||||||
|
domain: host_str,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CustomHttpClientError {
|
||||||
|
Blocked {
|
||||||
|
domain: String,
|
||||||
|
},
|
||||||
|
NonGlobalIp {
|
||||||
|
domain: Option<String>,
|
||||||
|
ip: IpAddr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomHttpClientError {
|
||||||
|
pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> {
|
||||||
|
let mut source = e.source();
|
||||||
|
|
||||||
|
while let Some(err) = source {
|
||||||
|
source = err.source();
|
||||||
|
if let Some(err) = err.downcast_ref::<CustomHttpClientError>() {
|
||||||
|
return Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CustomHttpClientError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Blocked {
|
||||||
|
domain,
|
||||||
|
} => write!(f, "Blocked domain: {domain} matched HTTP_REQUEST_BLOCK_REGEX"),
|
||||||
|
Self::NonGlobalIp {
|
||||||
|
domain: Some(domain),
|
||||||
|
ip,
|
||||||
|
} => write!(f, "IP {ip} for domain '{domain}' is not a global IP!"),
|
||||||
|
Self::NonGlobalIp {
|
||||||
|
domain: None,
|
||||||
|
ip,
|
||||||
|
} => write!(f, "IP {ip} is not a global IP!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CustomHttpClientError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum CustomDnsResolver {
|
||||||
|
Default(),
|
||||||
|
Hickory(Arc<TokioAsyncResolver>),
|
||||||
|
}
|
||||||
|
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
|
||||||
|
impl CustomDnsResolver {
|
||||||
|
fn instance() -> Arc<Self> {
|
||||||
|
static INSTANCE: Lazy<Arc<CustomDnsResolver>> = Lazy::new(CustomDnsResolver::new);
|
||||||
|
Arc::clone(&*INSTANCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new() -> Arc<Self> {
|
||||||
|
match read_system_conf() {
|
||||||
|
Ok((config, opts)) => {
|
||||||
|
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone());
|
||||||
|
Arc::new(Self::Hickory(Arc::new(resolver)))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error creating Hickory resolver, falling back to default: {e:?}");
|
||||||
|
Arc::new(Self::Default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
||||||
|
async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {
|
||||||
|
pre_resolve(name)?;
|
||||||
|
|
||||||
|
let result = match self {
|
||||||
|
Self::Default() => tokio::net::lookup_host(name).await?.next(),
|
||||||
|
Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(addr) = &result {
|
||||||
|
post_resolve(name, addr.ip())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pre_resolve(name: &str) -> Result<(), CustomHttpClientError> {
|
||||||
|
if should_block_address(name) {
|
||||||
|
return Err(CustomHttpClientError::Blocked {
|
||||||
|
domain: name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomHttpClientError> {
|
||||||
|
if should_block_ip(ip) {
|
||||||
|
Err(CustomHttpClientError::NonGlobalIp {
|
||||||
|
domain: Some(name.to_string()),
|
||||||
|
ip,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resolve for CustomDnsResolver {
|
||||||
|
fn resolve(&self, name: Name) -> Resolving {
|
||||||
|
let this = self.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let name = name.as_str();
|
||||||
|
let result = this.resolve_domain(name).await?;
|
||||||
|
Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
150
src/main.rs
150
src/main.rs
@@ -26,6 +26,7 @@ extern crate diesel;
|
|||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::{canonicalize, create_dir_all},
|
fs::{canonicalize, create_dir_all},
|
||||||
panic,
|
panic,
|
||||||
path::Path,
|
path::Path,
|
||||||
@@ -47,10 +48,12 @@ mod config;
|
|||||||
mod crypto;
|
mod crypto;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod db;
|
mod db;
|
||||||
|
mod http_client;
|
||||||
mod mail;
|
mod mail;
|
||||||
mod ratelimit;
|
mod ratelimit;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
|
||||||
use crate::api::purge_auth_requests;
|
use crate::api::purge_auth_requests;
|
||||||
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS};
|
||||||
pub use config::CONFIG;
|
pub use config::CONFIG;
|
||||||
@@ -64,19 +67,11 @@ async fn main() -> Result<(), Error> {
|
|||||||
parse_args();
|
parse_args();
|
||||||
launch_info();
|
launch_info();
|
||||||
|
|
||||||
use log::LevelFilter as LF;
|
let level = init_logging()?;
|
||||||
let level = LF::from_str(&CONFIG.log_level()).unwrap_or_else(|_| {
|
|
||||||
let valid_log_levels = LF::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::<Vec<String>>().join(", ");
|
|
||||||
println!("Log level must be one of the following: {valid_log_levels}");
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
init_logging(level).ok();
|
|
||||||
|
|
||||||
let extra_debug = matches!(level, LF::Trace | LF::Debug);
|
|
||||||
|
|
||||||
check_data_folder().await;
|
check_data_folder().await;
|
||||||
auth::initialize_keys().unwrap_or_else(|_| {
|
auth::initialize_keys().unwrap_or_else(|e| {
|
||||||
error!("Error creating keys, exiting...");
|
error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key());
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
check_web_vault();
|
check_web_vault();
|
||||||
@@ -90,6 +85,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
schedule_jobs(pool.clone());
|
schedule_jobs(pool.clone());
|
||||||
crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
|
crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&mut pool.get().await.unwrap()).await.unwrap();
|
||||||
|
|
||||||
|
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
|
||||||
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +206,38 @@ fn launch_info() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
fn init_logging() -> Result<log::LevelFilter, Error> {
|
||||||
|
let levels = log::LevelFilter::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::<Vec<String>>().join("|");
|
||||||
|
let log_level_rgx_str = format!("^({levels})((,[^,=]+=({levels}))*)$");
|
||||||
|
let log_level_rgx = regex::Regex::new(&log_level_rgx_str)?;
|
||||||
|
let config_str = CONFIG.log_level().to_lowercase();
|
||||||
|
|
||||||
|
let (level, levels_override) = if let Some(caps) = log_level_rgx.captures(&config_str) {
|
||||||
|
let level = caps
|
||||||
|
.get(1)
|
||||||
|
.and_then(|m| log::LevelFilter::from_str(m.as_str()).ok())
|
||||||
|
.ok_or(Error::new("Failed to parse global log level".to_string(), ""))?;
|
||||||
|
|
||||||
|
let levels_override: Vec<(&str, log::LevelFilter)> = caps
|
||||||
|
.get(2)
|
||||||
|
.map(|m| {
|
||||||
|
m.as_str()
|
||||||
|
.split(',')
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|s| match s.split('=').collect::<Vec<&str>>()[..] {
|
||||||
|
[log, lvl_str] => log::LevelFilter::from_str(lvl_str).ok().map(|lvl| (log, lvl)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.ok_or(Error::new("Failed to parse overrides".to_string(), ""))?;
|
||||||
|
|
||||||
|
(level, levels_override)
|
||||||
|
} else {
|
||||||
|
err!(format!("LOG_LEVEL should follow the format info,vaultwarden::api::icons=debug, invalid: {config_str}"))
|
||||||
|
};
|
||||||
|
|
||||||
// Depending on the main log level we either want to disable or enable logging for hickory.
|
// Depending on the main log level we either want to disable or enable logging for hickory.
|
||||||
// Else if there are timeouts it will clutter the logs since hickory uses warn for this.
|
// Else if there are timeouts it will clutter the logs since hickory uses warn for this.
|
||||||
let hickory_level = if level >= log::LevelFilter::Debug {
|
let hickory_level = if level >= log::LevelFilter::Debug {
|
||||||
@@ -241,47 +268,61 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
log::LevelFilter::Warn
|
log::LevelFilter::Warn
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut logger = fern::Dispatch::new()
|
|
||||||
.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("_", rocket_underscore_level)
|
|
||||||
.level_for("rocket::response::responder::_", rocket_underscore_level)
|
|
||||||
.level_for("rocket::server::_", rocket_underscore_level)
|
|
||||||
.level_for("vaultwarden::api::admin::_", rocket_underscore_level)
|
|
||||||
.level_for("vaultwarden::api::notifications::_", rocket_underscore_level)
|
|
||||||
// Silence Rocket logs
|
|
||||||
.level_for("rocket::launch", log::LevelFilter::Error)
|
|
||||||
.level_for("rocket::launch_", log::LevelFilter::Error)
|
|
||||||
.level_for("rocket::rocket", log::LevelFilter::Warn)
|
|
||||||
.level_for("rocket::server", log::LevelFilter::Warn)
|
|
||||||
.level_for("rocket::fairing::fairings", log::LevelFilter::Warn)
|
|
||||||
.level_for("rocket::shield::shield", log::LevelFilter::Warn)
|
|
||||||
.level_for("hyper::proto", log::LevelFilter::Off)
|
|
||||||
.level_for("hyper::client", log::LevelFilter::Off)
|
|
||||||
// Filter handlebars logs
|
|
||||||
.level_for("handlebars::render", handlebars_level)
|
|
||||||
// Prevent cookie_store logs
|
|
||||||
.level_for("cookie_store", log::LevelFilter::Off)
|
|
||||||
// Variable level for hickory used by reqwest
|
|
||||||
.level_for("hickory_resolver::name_server::name_server", hickory_level)
|
|
||||||
.level_for("hickory_proto::xfer", hickory_level)
|
|
||||||
.level_for("diesel_logger", diesel_logger_level)
|
|
||||||
.chain(std::io::stdout());
|
|
||||||
|
|
||||||
// Enable smtp debug logging only specifically for smtp when need.
|
// Enable smtp debug logging only specifically for smtp when need.
|
||||||
// This can contain sensitive information we do not want in the default debug/trace logging.
|
// This can contain sensitive information we do not want in the default debug/trace logging.
|
||||||
if CONFIG.smtp_debug() {
|
let smtp_log_level = if CONFIG.smtp_debug() {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Off
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut default_levels = HashMap::from([
|
||||||
|
// Hide unknown certificate errors if using self-signed
|
||||||
|
("rustls::session", log::LevelFilter::Off),
|
||||||
|
// Hide failed to close stream messages
|
||||||
|
("hyper::server", log::LevelFilter::Warn),
|
||||||
|
// Silence Rocket `_` logs
|
||||||
|
("_", rocket_underscore_level),
|
||||||
|
("rocket::response::responder::_", rocket_underscore_level),
|
||||||
|
("rocket::server::_", rocket_underscore_level),
|
||||||
|
("vaultwarden::api::admin::_", rocket_underscore_level),
|
||||||
|
("vaultwarden::api::notifications::_", rocket_underscore_level),
|
||||||
|
// Silence Rocket logs
|
||||||
|
("rocket::launch", log::LevelFilter::Error),
|
||||||
|
("rocket::launch_", log::LevelFilter::Error),
|
||||||
|
("rocket::rocket", log::LevelFilter::Warn),
|
||||||
|
("rocket::server", log::LevelFilter::Warn),
|
||||||
|
("rocket::fairing::fairings", log::LevelFilter::Warn),
|
||||||
|
("rocket::shield::shield", log::LevelFilter::Warn),
|
||||||
|
("hyper::proto", log::LevelFilter::Off),
|
||||||
|
("hyper::client", log::LevelFilter::Off),
|
||||||
|
// Filter handlebars logs
|
||||||
|
("handlebars::render", handlebars_level),
|
||||||
|
// Prevent cookie_store logs
|
||||||
|
("cookie_store", log::LevelFilter::Off),
|
||||||
|
// Variable level for hickory used by reqwest
|
||||||
|
("hickory_resolver::name_server::name_server", hickory_level),
|
||||||
|
("hickory_proto::xfer", hickory_level),
|
||||||
|
("diesel_logger", diesel_logger_level),
|
||||||
|
// SMTP
|
||||||
|
("lettre::transport::smtp", smtp_log_level),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (path, level) in levels_override.into_iter() {
|
||||||
|
let _ = default_levels.insert(path, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if Some(&log::LevelFilter::Debug) == default_levels.get("lettre::transport::smtp") {
|
||||||
println!(
|
println!(
|
||||||
"[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!\n\
|
"[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!\n\
|
||||||
[WARNING] Only enable SMTP_DEBUG during troubleshooting!\n"
|
[WARNING] Only enable SMTP_DEBUG during troubleshooting!\n"
|
||||||
);
|
);
|
||||||
logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Debug)
|
}
|
||||||
} else {
|
|
||||||
logger = logger.level_for("lettre::transport::smtp", log::LevelFilter::Off)
|
let mut logger = fern::Dispatch::new().level(level).chain(std::io::stdout());
|
||||||
|
|
||||||
|
for (path, level) in default_levels {
|
||||||
|
logger = logger.level_for(path.to_string(), level);
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.extended_logging() {
|
if CONFIG.extended_logging() {
|
||||||
@@ -318,7 +359,9 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.apply()?;
|
if let Err(err) = logger.apply() {
|
||||||
|
err!(format!("Failed to activate logger: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
// Catch panics and log them instead of default output to StdErr
|
// Catch panics and log them instead of default output to StdErr
|
||||||
panic::set_hook(Box::new(|info| {
|
panic::set_hook(Box::new(|info| {
|
||||||
@@ -356,7 +399,7 @@ fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok(())
|
Ok(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
@@ -513,7 +556,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler");
|
tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler");
|
||||||
info!("Exiting vaultwarden!");
|
info!("Exiting Vaultwarden!");
|
||||||
CONFIG.shutdown();
|
CONFIG.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -584,6 +627,13 @@ fn schedule_jobs(pool: db::DbPool) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean unused, expired Duo authentication contexts.
|
||||||
|
if !CONFIG.duo_context_purge_schedule().is_empty() && CONFIG._enable_duo() && !CONFIG.duo_use_iframe() {
|
||||||
|
sched.add(Job::new(CONFIG.duo_context_purge_schedule().parse().unwrap(), || {
|
||||||
|
runtime.spawn(purge_duo_contexts(pool.clone()));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup the event table of records x days old.
|
// Cleanup the event table of records x days old.
|
||||||
if CONFIG.org_events_enabled()
|
if CONFIG.org_events_enabled()
|
||||||
&& !CONFIG.event_cleanup_schedule().is_empty()
|
&& !CONFIG.event_cleanup_schedule().is_empty()
|
||||||
|
4
src/static/scripts/admin.js
vendored
4
src/static/scripts/admin.js
vendored
@@ -98,7 +98,7 @@ const showActiveTheme = (theme, focus = false) => {
|
|||||||
const themeSwitcherText = document.querySelector("#bd-theme-text");
|
const themeSwitcherText = document.querySelector("#bd-theme-text");
|
||||||
const activeThemeIcon = document.querySelector(".theme-icon-active use");
|
const activeThemeIcon = document.querySelector(".theme-icon-active use");
|
||||||
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
|
||||||
const svgOfActiveBtn = btnToActive.querySelector("span use").innerText;
|
const svgOfActiveBtn = btnToActive.querySelector("span use").textContent;
|
||||||
|
|
||||||
document.querySelectorAll("[data-bs-theme-value]").forEach(element => {
|
document.querySelectorAll("[data-bs-theme-value]").forEach(element => {
|
||||||
element.classList.remove("active");
|
element.classList.remove("active");
|
||||||
@@ -107,7 +107,7 @@ const showActiveTheme = (theme, focus = false) => {
|
|||||||
|
|
||||||
btnToActive.classList.add("active");
|
btnToActive.classList.add("active");
|
||||||
btnToActive.setAttribute("aria-pressed", "true");
|
btnToActive.setAttribute("aria-pressed", "true");
|
||||||
activeThemeIcon.innerText = svgOfActiveBtn;
|
activeThemeIcon.textContent = svgOfActiveBtn;
|
||||||
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
|
||||||
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
|
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
|
||||||
|
|
||||||
|
10
src/static/scripts/admin_diagnostics.js
vendored
10
src/static/scripts/admin_diagnostics.js
vendored
@@ -117,7 +117,7 @@ async function generateSupportString(event, dj) {
|
|||||||
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`;
|
||||||
supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
|
supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n";
|
||||||
|
|
||||||
document.getElementById("support-string").innerText = supportString;
|
document.getElementById("support-string").textContent = supportString;
|
||||||
document.getElementById("support-string").classList.remove("d-none");
|
document.getElementById("support-string").classList.remove("d-none");
|
||||||
document.getElementById("copy-support").classList.remove("d-none");
|
document.getElementById("copy-support").classList.remove("d-none");
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ function copyToClipboard(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const supportStr = document.getElementById("support-string").innerText;
|
const supportStr = document.getElementById("support-string").textContent;
|
||||||
const tmpCopyEl = document.createElement("textarea");
|
const tmpCopyEl = document.createElement("textarea");
|
||||||
|
|
||||||
tmpCopyEl.setAttribute("id", "copy-support-string");
|
tmpCopyEl.setAttribute("id", "copy-support-string");
|
||||||
@@ -201,7 +201,7 @@ function checkDns(dns_resolved) {
|
|||||||
|
|
||||||
function init(dj) {
|
function init(dj) {
|
||||||
// Time check
|
// Time check
|
||||||
document.getElementById("time-browser-string").innerText = browserUTC;
|
document.getElementById("time-browser-string").textContent = browserUTC;
|
||||||
|
|
||||||
// Check if we were able to fetch a valid NTP Time
|
// Check if we were able to fetch a valid NTP Time
|
||||||
// If so, compare both browser and server with NTP
|
// If so, compare both browser and server with NTP
|
||||||
@@ -217,7 +217,7 @@ function init(dj) {
|
|||||||
|
|
||||||
// Domain check
|
// Domain check
|
||||||
const browserURL = location.href.toLowerCase();
|
const browserURL = location.href.toLowerCase();
|
||||||
document.getElementById("domain-browser-string").innerText = browserURL;
|
document.getElementById("domain-browser-string").textContent = browserURL;
|
||||||
checkDomain(browserURL, dj.admin_url.toLowerCase());
|
checkDomain(browserURL, dj.admin_url.toLowerCase());
|
||||||
|
|
||||||
// Version check
|
// Version check
|
||||||
@@ -229,7 +229,7 @@ function init(dj) {
|
|||||||
|
|
||||||
// onLoad events
|
// onLoad events
|
||||||
document.addEventListener("DOMContentLoaded", (event) => {
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
const diag_json = JSON.parse(document.getElementById("diagnostics_json").innerText);
|
const diag_json = JSON.parse(document.getElementById("diagnostics_json").textContent);
|
||||||
init(diag_json);
|
init(diag_json);
|
||||||
|
|
||||||
const btnGenSupport = document.getElementById("gen-support");
|
const btnGenSupport = document.getElementById("gen-support");
|
||||||
|
2
src/static/scripts/admin_settings.js
vendored
2
src/static/scripts/admin_settings.js
vendored
@@ -122,7 +122,7 @@ function submitTestEmailOnEnter() {
|
|||||||
function colorRiskSettings() {
|
function colorRiskSettings() {
|
||||||
const risk_items = document.getElementsByClassName("col-form-label");
|
const risk_items = document.getElementsByClassName("col-form-label");
|
||||||
Array.from(risk_items).forEach((el) => {
|
Array.from(risk_items).forEach((el) => {
|
||||||
if (el.innerText.toLowerCase().includes("risks") ) {
|
if (el.textContent.toLowerCase().includes("risks") ) {
|
||||||
el.parentElement.className += " alert-danger";
|
el.parentElement.className += " alert-danger";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
6
src/static/scripts/admin_users.js
vendored
6
src/static/scripts/admin_users.js
vendored
@@ -198,7 +198,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) {
|
|||||||
const orgName = event.relatedTarget.dataset.vwOrgName;
|
const orgName = event.relatedTarget.dataset.vwOrgName;
|
||||||
const orgUuid = event.relatedTarget.dataset.vwOrgUuid;
|
const orgUuid = event.relatedTarget.dataset.vwOrgUuid;
|
||||||
|
|
||||||
document.getElementById("userOrgTypeDialogTitle").innerHTML = `<b>Update User Type:</b><br><b>Organization:</b> ${orgName}<br><b>User:</b> ${userEmail}`;
|
document.getElementById("userOrgTypeDialogOrgName").textContent = orgName;
|
||||||
|
document.getElementById("userOrgTypeDialogUserEmail").textContent = userEmail;
|
||||||
document.getElementById("userOrgTypeUserUuid").value = userUuid;
|
document.getElementById("userOrgTypeUserUuid").value = userUuid;
|
||||||
document.getElementById("userOrgTypeOrgUuid").value = orgUuid;
|
document.getElementById("userOrgTypeOrgUuid").value = orgUuid;
|
||||||
document.getElementById(`userOrgType${userOrgTypeName}`).checked = true;
|
document.getElementById(`userOrgType${userOrgTypeName}`).checked = true;
|
||||||
@@ -206,7 +207,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) {
|
|||||||
|
|
||||||
// Prevent accidental submission of the form with valid elements after the modal has been hidden.
|
// Prevent accidental submission of the form with valid elements after the modal has been hidden.
|
||||||
userOrgTypeDialog.addEventListener("hide.bs.modal", function() {
|
userOrgTypeDialog.addEventListener("hide.bs.modal", function() {
|
||||||
document.getElementById("userOrgTypeDialogTitle").innerHTML = "";
|
document.getElementById("userOrgTypeDialogOrgName").textContent = "";
|
||||||
|
document.getElementById("userOrgTypeDialogUserEmail").textContent = "";
|
||||||
document.getElementById("userOrgTypeUserUuid").value = "";
|
document.getElementById("userOrgTypeUserUuid").value = "";
|
||||||
document.getElementById("userOrgTypeOrgUuid").value = "";
|
document.getElementById("userOrgTypeOrgUuid").value = "";
|
||||||
}, false);
|
}, false);
|
||||||
|
4
src/static/scripts/datatables.css
vendored
4
src/static/scripts/datatables.css
vendored
@@ -4,10 +4,10 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.0.7
|
* https://datatables.net/download/#bs5/dt-2.0.8
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.0.7
|
* DataTables 2.0.8
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
|
53
src/static/scripts/datatables.js
vendored
53
src/static/scripts/datatables.js
vendored
@@ -4,20 +4,20 @@
|
|||||||
*
|
*
|
||||||
* To rebuild or modify this file with the latest versions of the included
|
* To rebuild or modify this file with the latest versions of the included
|
||||||
* software please visit:
|
* software please visit:
|
||||||
* https://datatables.net/download/#bs5/dt-2.0.7
|
* https://datatables.net/download/#bs5/dt-2.0.8
|
||||||
*
|
*
|
||||||
* Included libraries:
|
* Included libraries:
|
||||||
* DataTables 2.0.7
|
* DataTables 2.0.8
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*! DataTables 2.0.7
|
/*! DataTables 2.0.8
|
||||||
* © SpryMedia Ltd - datatables.net/license
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary DataTables
|
* @summary DataTables
|
||||||
* @description Paginate, search and order HTML tables
|
* @description Paginate, search and order HTML tables
|
||||||
* @version 2.0.7
|
* @version 2.0.8
|
||||||
* @author SpryMedia Ltd
|
* @author SpryMedia Ltd
|
||||||
* @contact www.datatables.net
|
* @contact www.datatables.net
|
||||||
* @copyright SpryMedia Ltd.
|
* @copyright SpryMedia Ltd.
|
||||||
@@ -563,7 +563,7 @@
|
|||||||
*
|
*
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
builder: "bs5/dt-2.0.7",
|
builder: "bs5/dt-2.0.8",
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7572,6 +7572,16 @@
|
|||||||
order = opts.order, // applied, current, index (original - compatibility with 1.9)
|
order = opts.order, // applied, current, index (original - compatibility with 1.9)
|
||||||
page = opts.page; // all, current
|
page = opts.page; // all, current
|
||||||
|
|
||||||
|
if ( _fnDataSource( settings ) == 'ssp' ) {
|
||||||
|
// In server-side processing mode, most options are irrelevant since
|
||||||
|
// rows not shown don't exist and the index order is the applied order
|
||||||
|
// Removed is a special case - for consistency just return an empty
|
||||||
|
// array
|
||||||
|
return search === 'removed' ?
|
||||||
|
[] :
|
||||||
|
_range( 0, displayMaster.length );
|
||||||
|
}
|
||||||
|
|
||||||
if ( page == 'current' ) {
|
if ( page == 'current' ) {
|
||||||
// Current page implies that order=current and filter=applied, since it is
|
// Current page implies that order=current and filter=applied, since it is
|
||||||
// fairly senseless otherwise, regardless of what order and search actually
|
// fairly senseless otherwise, regardless of what order and search actually
|
||||||
@@ -8243,7 +8253,7 @@
|
|||||||
_api_register( _child_obj+'.isShown()', function () {
|
_api_register( _child_obj+'.isShown()', function () {
|
||||||
var ctx = this.context;
|
var ctx = this.context;
|
||||||
|
|
||||||
if ( ctx.length && this.length ) {
|
if ( ctx.length && this.length && ctx[0].aoData[ this[0] ] ) {
|
||||||
// _detailsShown as false or undefined will fall through to return false
|
// _detailsShown as false or undefined will fall through to return false
|
||||||
return ctx[0].aoData[ this[0] ]._detailsShow || false;
|
return ctx[0].aoData[ this[0] ]._detailsShow || false;
|
||||||
}
|
}
|
||||||
@@ -8266,7 +8276,7 @@
|
|||||||
// can be an array of these items, comma separated list, or an array of comma
|
// can be an array of these items, comma separated list, or an array of comma
|
||||||
// separated lists
|
// separated lists
|
||||||
|
|
||||||
var __re_column_selector = /^([^:]+):(name|title|visIdx|visible)$/;
|
var __re_column_selector = /^([^:]+)?:(name|title|visIdx|visible)$/;
|
||||||
|
|
||||||
|
|
||||||
// r1 and r2 are redundant - but it means that the parameters match for the
|
// r1 and r2 are redundant - but it means that the parameters match for the
|
||||||
@@ -8338,17 +8348,24 @@
|
|||||||
switch( match[2] ) {
|
switch( match[2] ) {
|
||||||
case 'visIdx':
|
case 'visIdx':
|
||||||
case 'visible':
|
case 'visible':
|
||||||
var idx = parseInt( match[1], 10 );
|
if (match[1]) {
|
||||||
// Visible index given, convert to column index
|
var idx = parseInt( match[1], 10 );
|
||||||
if ( idx < 0 ) {
|
// Visible index given, convert to column index
|
||||||
// Counting from the right
|
if ( idx < 0 ) {
|
||||||
var visColumns = columns.map( function (col,i) {
|
// Counting from the right
|
||||||
return col.bVisible ? i : null;
|
var visColumns = columns.map( function (col,i) {
|
||||||
} );
|
return col.bVisible ? i : null;
|
||||||
return [ visColumns[ visColumns.length + idx ] ];
|
} );
|
||||||
|
return [ visColumns[ visColumns.length + idx ] ];
|
||||||
|
}
|
||||||
|
// Counting from the left
|
||||||
|
return [ _fnVisibleToColumnIndex( settings, idx ) ];
|
||||||
}
|
}
|
||||||
// Counting from the left
|
|
||||||
return [ _fnVisibleToColumnIndex( settings, idx ) ];
|
// `:visible` on its own
|
||||||
|
return columns.map( function (col, i) {
|
||||||
|
return col.bVisible ? i : null;
|
||||||
|
} );
|
||||||
|
|
||||||
case 'name':
|
case 'name':
|
||||||
// match by name. `names` is column index complete and in order
|
// match by name. `names` is column index complete and in order
|
||||||
@@ -9623,7 +9640,7 @@
|
|||||||
* @type string
|
* @type string
|
||||||
* @default Version number
|
* @default Version number
|
||||||
*/
|
*/
|
||||||
DataTable.version = "2.0.7";
|
DataTable.version = "2.0.8";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private data store, containing all of the settings objects that are
|
* Private data store, containing all of the settings objects that are
|
||||||
|
@@ -44,7 +44,7 @@
|
|||||||
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
|
<span class="d-block"><strong>Events:</strong> {{event_count}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-0 small">
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc id no_quote}}" data-vw-org-name="{{jsesc name no_quote}}" data-vw-billing-email="{{jsesc billingEmail no_quote}}">Delete Organization</button><br>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}" data-vw-billing-email="{{billingEmail}}">Delete Organization</button><br>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
@@ -54,14 +54,14 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="overflow-auto vw-org-cell" data-vw-user-email="{{jsesc email no_quote}}" data-vw-user-uuid="{{jsesc id no_quote}}">
|
<div class="overflow-auto vw-org-cell" data-vw-user-email="{{email}}" data-vw-user-uuid="{{id}}">
|
||||||
{{#each organizations}}
|
{{#each organizations}}
|
||||||
<button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{type}}" data-vw-org-uuid="{{jsesc id no_quote}}" data-vw-org-name="{{jsesc name no_quote}}">{{name}}</button>
|
<button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{type}}" data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}">{{name}}</button>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end px-0 small">
|
<td class="text-end px-0 small">
|
||||||
<span data-vw-user-uuid="{{jsesc id no_quote}}" data-vw-user-email="{{jsesc email no_quote}}">
|
<span data-vw-user-uuid="{{id}}" data-vw-user-email="{{email}}">
|
||||||
{{#if twoFactorEnabled}}
|
{{#if twoFactorEnabled}}
|
||||||
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>
|
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -109,7 +109,9 @@
|
|||||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
|
<h6 class="modal-title">
|
||||||
|
<b>Update User Type:</b><br><b>Organization:</b> <span id="userOrgTypeDialogOrgName"></span><br><b>User:</b> <span id="userOrgTypeDialogUserEmail"></span>
|
||||||
|
</h6>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form class="form" id="userOrgTypeForm">
|
<form class="form" id="userOrgTypeForm">
|
||||||
|
149
src/util.rs
149
src/util.rs
@@ -4,7 +4,6 @@
|
|||||||
use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path};
|
use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path};
|
||||||
|
|
||||||
use num_traits::ToPrimitive;
|
use num_traits::ToPrimitive;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fairing::{Fairing, Info, Kind},
|
fairing::{Fairing, Info, Kind},
|
||||||
http::{ContentType, Header, HeaderMap, Method, Status},
|
http::{ContentType, Header, HeaderMap, Method, Status},
|
||||||
@@ -686,19 +685,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use reqwest::{header, Client, ClientBuilder};
|
|
||||||
|
|
||||||
pub fn get_reqwest_client() -> &'static Client {
|
|
||||||
static INSTANCE: Lazy<Client> = Lazy::new(|| get_reqwest_client_builder().build().expect("Failed to build client"));
|
|
||||||
&INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_reqwest_client_builder() -> ClientBuilder {
|
|
||||||
let mut headers = header::HeaderMap::new();
|
|
||||||
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden"));
|
|
||||||
Client::builder().default_headers(headers).timeout(Duration::from_secs(10))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
||||||
match src_json {
|
match src_json {
|
||||||
Value::Array(elm) => {
|
Value::Array(elm) => {
|
||||||
@@ -744,144 +730,11 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
|||||||
|
|
||||||
/// Parses the experimental client feature flags string into a HashMap.
|
/// Parses the experimental client feature flags string into a HashMap.
|
||||||
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
|
pub fn parse_experimental_client_feature_flags(experimental_client_feature_flags: &str) -> HashMap<String, bool> {
|
||||||
let feature_states =
|
let feature_states = experimental_client_feature_flags.split(',').map(|f| (f.trim().to_owned(), true)).collect();
|
||||||
experimental_client_feature_flags.to_lowercase().split(',').map(|f| (f.trim().to_owned(), true)).collect();
|
|
||||||
|
|
||||||
feature_states
|
feature_states
|
||||||
}
|
}
|
||||||
|
|
||||||
mod dns_resolver {
|
|
||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
net::{IpAddr, SocketAddr},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use hickory_resolver::{system_conf::read_system_conf, TokioAsyncResolver};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use reqwest::dns::{Name, Resolve, Resolving};
|
|
||||||
|
|
||||||
use crate::{util::is_global, CONFIG};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CustomResolverError {
|
|
||||||
Blacklist {
|
|
||||||
domain: String,
|
|
||||||
},
|
|
||||||
NonGlobalIp {
|
|
||||||
domain: String,
|
|
||||||
ip: IpAddr,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CustomResolverError {
|
|
||||||
pub fn downcast_ref(e: &dyn std::error::Error) -> Option<&Self> {
|
|
||||||
let mut source = e.source();
|
|
||||||
|
|
||||||
while let Some(err) = source {
|
|
||||||
source = err.source();
|
|
||||||
if let Some(err) = err.downcast_ref::<CustomResolverError>() {
|
|
||||||
return Some(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CustomResolverError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Blacklist {
|
|
||||||
domain,
|
|
||||||
} => write!(f, "Blacklisted domain: {domain} matched ICON_BLACKLIST_REGEX"),
|
|
||||||
Self::NonGlobalIp {
|
|
||||||
domain,
|
|
||||||
ip,
|
|
||||||
} => write!(f, "IP {ip} for domain '{domain}' is not a global IP!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for CustomResolverError {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CustomDnsResolver {
|
|
||||||
Default(),
|
|
||||||
Hickory(Arc<TokioAsyncResolver>),
|
|
||||||
}
|
|
||||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
|
||||||
|
|
||||||
impl CustomDnsResolver {
|
|
||||||
pub fn instance() -> Arc<Self> {
|
|
||||||
static INSTANCE: Lazy<Arc<CustomDnsResolver>> = Lazy::new(CustomDnsResolver::new);
|
|
||||||
Arc::clone(&*INSTANCE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new() -> Arc<Self> {
|
|
||||||
match read_system_conf() {
|
|
||||||
Ok((config, opts)) => {
|
|
||||||
let resolver = TokioAsyncResolver::tokio(config.clone(), opts.clone());
|
|
||||||
Arc::new(Self::Hickory(Arc::new(resolver)))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error creating Hickory resolver, falling back to default: {e:?}");
|
|
||||||
Arc::new(Self::Default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that we get an iterator of addresses, but we only grab the first one for convenience
|
|
||||||
async fn resolve_domain(&self, name: &str) -> Result<Option<SocketAddr>, BoxError> {
|
|
||||||
pre_resolve(name)?;
|
|
||||||
|
|
||||||
let result = match self {
|
|
||||||
Self::Default() => tokio::net::lookup_host(name).await?.next(),
|
|
||||||
Self::Hickory(r) => r.lookup_ip(name).await?.iter().next().map(|a| SocketAddr::new(a, 0)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(addr) = &result {
|
|
||||||
post_resolve(name, addr.ip())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pre_resolve(name: &str) -> Result<(), CustomResolverError> {
|
|
||||||
if crate::api::is_domain_blacklisted(name) {
|
|
||||||
return Err(CustomResolverError::Blacklist {
|
|
||||||
domain: name.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn post_resolve(name: &str, ip: IpAddr) -> Result<(), CustomResolverError> {
|
|
||||||
if CONFIG.icon_blacklist_non_global_ips() && !is_global(ip) {
|
|
||||||
Err(CustomResolverError::NonGlobalIp {
|
|
||||||
domain: name.to_string(),
|
|
||||||
ip,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resolve for CustomDnsResolver {
|
|
||||||
fn resolve(&self, name: Name) -> Resolving {
|
|
||||||
let this = self.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let name = name.as_str();
|
|
||||||
let result = this.resolve_domain(name).await?;
|
|
||||||
Ok::<reqwest::dns::Addrs, _>(Box::new(result.into_iter()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use dns_resolver::{CustomDnsResolver, CustomResolverError};
|
|
||||||
|
|
||||||
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
|
||||||
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
|
||||||
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
|
||||||
|
Reference in New Issue
Block a user