mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 17:13:02 +03:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743db47f90 | ||
|
|
a476c15a09 | ||
|
|
0d74fa3ec5 | ||
|
|
b83cc6938b | ||
|
|
a187b14885 | ||
|
|
d3de3db0d9 | ||
|
|
80ac9afe68 | ||
|
|
4ef07737e1 | ||
|
|
5782c5f413 | ||
|
|
8b4cab1e69 | ||
|
|
70300b133c | ||
|
|
7ea4bb808d | ||
|
|
6766e0d437 | ||
|
|
53c9bc8bea | ||
|
|
60df3b473c | ||
|
|
6510ae8be9 | ||
|
|
9545fb6e37 | ||
|
|
0fd0f11298 | ||
|
|
46aa8fcc02 | ||
|
|
09bb738866 | ||
|
|
3612ef10d1 | ||
|
|
7ac2039a36 | ||
|
|
7f83de765a | ||
|
|
9b3779b13a | ||
|
|
11a52f29c4 | ||
|
|
10204c723f | ||
|
|
204421643d | ||
|
|
d9706d75ef | ||
|
|
40df0bd2f9 | ||
|
|
a53411b4d6 | ||
|
|
609017b2f5 | ||
|
|
7dc0b0e218 | ||
|
|
6be36b8e51 | ||
|
|
8be545d3da | ||
|
|
4f3a8d275b | ||
|
|
9c412f4276 | ||
|
|
27c269d6a0 | ||
|
|
57b4a74279 |
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -29,16 +29,12 @@ jobs:
|
|||||||
RUSTFLAGS: --deny warnings
|
RUSTFLAGS: --deny warnings
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Rust Toolchain Components
|
- name: Install Rust Toolchain Components
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
|
||||||
components: clippy, rustfmt
|
|
||||||
override: true
|
|
||||||
toolchain: stable
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --all
|
run: cargo test --all
|
||||||
|
|||||||
82
.github/workflows/release.yaml
vendored
82
.github/workflows/release.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- target: aarch64-pc-windows-msvc
|
- target: aarch64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: "--no-default-features"
|
cargo-flags: ""
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
cargo-flags: ""
|
cargo-flags: ""
|
||||||
@@ -71,30 +71,42 @@ jobs:
|
|||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: "--no-default-features"
|
cargo-flags: "--no-default-features"
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
|
env:
|
||||||
|
BUILD_CMD: cargo
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Check Tag
|
- name: Check Tag
|
||||||
id: check-tag
|
id: check-tag
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=${GITHUB_REF##*/}
|
ver=${GITHUB_REF##*/}
|
||||||
echo "::set-output name=version::$tag"
|
echo "version=$ver" >> $GITHUB_OUTPUT
|
||||||
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
|
if [[ "$ver" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
|
||||||
echo "::set-output name=rc::false"
|
echo "rc=false" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "::set-output name=rc::true"
|
echo "rc=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
- name: Install Rust Toolchain Components
|
- name: Install Rust Toolchain Components
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
override: true
|
targets: ${{ matrix.target }}
|
||||||
target: ${{ matrix.target }}
|
# Since rust 1.72, mips platforms are tier 3
|
||||||
toolchain: stable
|
toolchain: 1.71
|
||||||
profile: minimal # minimal component installation (ie, no documentation)
|
|
||||||
|
- name: Install cross
|
||||||
|
if: matrix.use-cross
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: cross
|
||||||
|
|
||||||
|
- name: Overwrite build command env variable
|
||||||
|
if: matrix.use-cross
|
||||||
|
shell: bash
|
||||||
|
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Show Version Information (Rust, cargo, GCC)
|
- name: Show Version Information (Rust, cargo, GCC)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -107,11 +119,8 @@ jobs:
|
|||||||
rustc -V
|
rustc -V
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions-rs/cargo@v1
|
shell: bash
|
||||||
with:
|
run: $BUILD_CMD build --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
|
||||||
use-cross: ${{ matrix.use-cross }}
|
|
||||||
command: build
|
|
||||||
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
|
|
||||||
|
|
||||||
- name: Build Archive
|
- name: Build Archive
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -123,8 +132,7 @@ jobs:
|
|||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
bin=${GITHUB_REPOSITORY##*/}
|
bin=${GITHUB_REPOSITORY##*/}
|
||||||
src=`pwd`
|
dist_dir=`pwd`/dist
|
||||||
dist=$src/dist
|
|
||||||
name=$bin-$version-$target
|
name=$bin-$version-$target
|
||||||
executable=target/$target/release/$bin
|
executable=target/$target/release/$bin
|
||||||
|
|
||||||
@@ -132,22 +140,22 @@ jobs:
|
|||||||
executable=$executable.exe
|
executable=$executable.exe
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir $dist
|
mkdir $dist_dir
|
||||||
cp $executable $dist
|
cp $executable $dist_dir
|
||||||
cd $dist
|
cd $dist_dir
|
||||||
|
|
||||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||||
archive=$dist/$name.zip
|
archive=$dist_dir/$name.zip
|
||||||
7z a $archive *
|
7z a $archive *
|
||||||
echo "::set-output name=archive::`pwd -W`/$name.zip"
|
echo "archive=dist/$name.zip" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
archive=$dist/$name.tar.gz
|
archive=$dist_dir/$name.tar.gz
|
||||||
tar czf $archive *
|
tar -czf $archive *
|
||||||
echo "::set-output name=archive::$archive"
|
echo "archive=dist/$name.tar.gz" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v0.1.5
|
uses: softprops/action-gh-release@v1
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
@@ -163,16 +171,16 @@ jobs:
|
|||||||
needs: release
|
needs: release
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
build-args: |
|
build-args: |
|
||||||
REPO=${{ github.repository }}
|
REPO=${{ github.repository }}
|
||||||
@@ -191,13 +199,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: release
|
needs: release
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
- name: Publish
|
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
env:
|
env:
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
|
||||||
run: cargo publish
|
run: cargo publish
|
||||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -2,6 +2,71 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.37.1] - 2023-11-08
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use DUFS_CONFIG to specify the config file path ([#286](https://github.com/sigoden/dufs/issues/286)
|
||||||
|
|
||||||
|
## [0.37.0] - 2023-11-08
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Sort path ignore case ([#264](https://github.com/sigoden/dufs/issues/264))
|
||||||
|
- Ui show user-name next to the user-icon ([#278](https://github.com/sigoden/dufs/issues/278))
|
||||||
|
- Auto delete half-uploaded files ([#280](https://github.com/sigoden/dufs/issues/280))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Deprecate `--auth-method`, as both options are available ([#279](https://github.com/sigoden/dufs/issues/279))
|
||||||
|
- Support config file with `--config` option ([#281](https://github.com/sigoden/dufs/issues/281))
|
||||||
|
- Support hashed password ([#283](https://github.com/sigoden/dufs/issues/283))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Remove one clone on `assets_prefix` ([#270](https://github.com/sigoden/dufs/issues/270))
|
||||||
|
- Optimize tests
|
||||||
|
- Improve code quanity ([#282](https://github.com/sigoden/dufs/issues/282))
|
||||||
|
|
||||||
|
## [0.36.0] - 2023-08-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Ui readonly if no write perm ([#258](https://github.com/sigoden/dufs/issues/258))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Remove dependency on native tls ([#255](https://github.com/sigoden/dufs/issues/255))
|
||||||
|
|
||||||
|
## [0.35.0] - 2023-08-14
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Search should ignore entry path ([#235](https://github.com/sigoden/dufs/issues/235))
|
||||||
|
- Typo __ASSERTS_PREFIX__ ([#252](https://github.com/sigoden/dufs/issues/252))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Sort by type first, then sort by name/mtime/size ([#241](https://github.com/sigoden/dufs/issues/241))
|
||||||
|
|
||||||
|
## [0.34.2] - 2023-06-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Ui refresh page after login ([#230](https://github.com/sigoden/dufs/issues/230))
|
||||||
|
- Webdav only see public folder even logging in ([#231](https://github.com/sigoden/dufs/issues/231))
|
||||||
|
|
||||||
|
## [0.34.1] - 2023-06-02
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Auth logic ([#224](https://github.com/sigoden/dufs/issues/224))
|
||||||
|
- Allow all cors headers and methods ([#225](https://github.com/sigoden/dufs/issues/225))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Ui checkAuth ([#226](https://github.com/sigoden/dufs/issues/226))
|
||||||
|
|
||||||
## [0.34.0] - 2023-06-01
|
## [0.34.0] - 2023-06-01
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
1115
Cargo.lock
generated
1115
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dufs"
|
name = "dufs"
|
||||||
version = "0.34.0"
|
version = "0.37.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["sigoden <sigoden@gmail.com>"]
|
authors = ["sigoden <sigoden@gmail.com>"]
|
||||||
description = "Dufs is a distinctive utility file server"
|
description = "Dufs is a distinctive utility file server"
|
||||||
@@ -13,15 +13,14 @@ keywords = ["static", "file", "server", "webdav", "cli"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["wrap_help", "env"] }
|
clap = { version = "4", features = ["wrap_help", "env"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
|
||||||
tokio-util = { version = "0.7", features = ["io-util", "compat"] }
|
tokio-util = { version = "0.7", features = ["io-util", "compat"] }
|
||||||
hyper = { version = "0.14", features = ["http1", "server", "tcp", "stream"] }
|
hyper = { version = "0.14", features = ["http1", "server", "tcp", "stream"] }
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
base64 = "0.21"
|
|
||||||
async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] }
|
async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] }
|
||||||
headers = "0.3"
|
headers = "0.3"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
@@ -31,20 +30,23 @@ rustls-pemfile = { version = "1", optional = true }
|
|||||||
tokio-rustls = { version = "0.24", optional = true }
|
tokio-rustls = { version = "0.24", optional = true }
|
||||||
md5 = "0.7"
|
md5 = "0.7"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
uuid = { version = "1.1", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.4", features = ["v4", "fast-rng"] }
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
xml-rs = "0.8"
|
xml-rs = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
socket2 = "0.5"
|
socket2 = "0.5"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
walkdir = "2.3"
|
walkdir = "2.3"
|
||||||
form_urlencoded = "1.0"
|
form_urlencoded = "1.2"
|
||||||
alphanumeric-sort = "1.4"
|
alphanumeric-sort = "1.4"
|
||||||
content_inspector = "0.2"
|
content_inspector = "0.2"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
chardetng = "0.1"
|
chardetng = "0.1"
|
||||||
glob = "0.3.1"
|
glob = "0.3.1"
|
||||||
indexmap = "1.9"
|
indexmap = "2.0"
|
||||||
|
serde_yaml = "0.9.27"
|
||||||
|
sha-crypt = "0.5.0"
|
||||||
|
base64 = "0.21.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls"]
|
default = ["tls"]
|
||||||
@@ -55,13 +57,15 @@ assert_cmd = "2"
|
|||||||
reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
|
||||||
assert_fs = "1"
|
assert_fs = "1"
|
||||||
port_check = "0.1"
|
port_check = "0.1"
|
||||||
rstest = "0.17"
|
rstest = "0.18"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
diqwest = { version = "1", features = ["blocking"] }
|
diqwest = { version = "1", features = ["blocking", "rustls-tls"], default-features = false }
|
||||||
predicates = "3"
|
predicates = "3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
strip = true
|
codegen-units = 1
|
||||||
opt-level = "z"
|
panic = "abort"
|
||||||
|
strip = "symbols"
|
||||||
|
|||||||
104
README.md
104
README.md
@@ -48,18 +48,18 @@ Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip
|
|||||||
```
|
```
|
||||||
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
|
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
|
||||||
|
|
||||||
Usage: dufs [OPTIONS] [serve_path]
|
Usage: dufs [OPTIONS] [serve-path]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
[serve_path] Specific path to serve [default: .]
|
[serve-path] Specific path to serve [default: .]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
-c, --config <config> Specify configuration file
|
||||||
-b, --bind <addrs> Specify bind address or unix socket
|
-b, --bind <addrs> Specify bind address or unix socket
|
||||||
-p, --port <port> Specify port to listen on [default: 5000]
|
-p, --port <port> Specify port to listen on [default: 5000]
|
||||||
--path-prefix <path> Specify a path prefix
|
--path-prefix <path> Specify a path prefix
|
||||||
--hidden <value> Hide paths from directory listings, separated by `,`
|
--hidden <value> Hide paths from directory listings, e.g. tmp,*.log,*.lock
|
||||||
-a, --auth <rules> Add auth role
|
-a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2
|
||||||
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
|
|
||||||
-A, --allow-all Allow all operations
|
-A, --allow-all Allow all operations
|
||||||
--allow-upload Allow upload files/folders
|
--allow-upload Allow upload files/folders
|
||||||
--allow-delete Allow delete files/folders
|
--allow-delete Allow delete files/folders
|
||||||
@@ -70,11 +70,11 @@ Options:
|
|||||||
--render-index Serve index.html when requesting a directory, returns 404 if not found index.html
|
--render-index Serve index.html when requesting a directory, returns 404 if not found index.html
|
||||||
--render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
|
--render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
|
||||||
--render-spa Serve SPA(Single Page Application)
|
--render-spa Serve SPA(Single Page Application)
|
||||||
--assets <path> Use custom assets to override builtin assets
|
--assets <path> Set the path to the assets directory for overriding the built-in assets
|
||||||
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
|
|
||||||
--tls-key <path> Path to the SSL/TLS certificate's private key
|
|
||||||
--log-format <format> Customize http log format
|
--log-format <format> Customize http log format
|
||||||
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
|
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
|
||||||
|
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
|
||||||
|
--tls-key <path> Path to the SSL/TLS certificate's private key
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
-V, --version Print version
|
-V, --version Print version
|
||||||
```
|
```
|
||||||
@@ -126,7 +126,7 @@ dufs --render-index
|
|||||||
Require username/password
|
Require username/password
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs -a /@admin:123
|
dufs -a admin:123@/:rw
|
||||||
```
|
```
|
||||||
|
|
||||||
Listen on specific host:ip
|
Listen on specific host:ip
|
||||||
@@ -194,8 +194,8 @@ curl http://127.0.0.1:5000?q=Dockerfile&simple # search for files, just like
|
|||||||
With authorization
|
With authorization
|
||||||
|
|
||||||
```
|
```
|
||||||
curl --user user:pass --digest http://192.168.8.10:5000/file # digest auth
|
curl http://192.168.8.10:5000/file --user user:pass # basic auth
|
||||||
curl --user user:pass http://192.168.8.10:5000/file # basic auth
|
curl http://192.168.8.10:5000/file --user user:pass --digest # digest auth
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -206,12 +206,14 @@ curl --user user:pass http://192.168.8.10:5000/file # basic auth
|
|||||||
Dufs supports account based access control. You can control who can do what on which path with `--auth`/`-a`.
|
Dufs supports account based access control. You can control who can do what on which path with `--auth`/`-a`.
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs -a [user:pass]@path[:rw][,path[:rw]...][|...]
|
dufs -a user:pass@path1:rw,path2|user2:pass2@path1
|
||||||
|
dufs -a user:pass@path1:rw,path2 -a user2:pass2@path1
|
||||||
```
|
```
|
||||||
1: Multiple rules are separated by "|"
|
|
||||||
2: User and pass are the account name and password, if omitted, it is an anonymous user
|
1. Multiple rules are separated by "|"
|
||||||
3: One rule can set multiple paths, separated by ","
|
2. User and pass are the account name and password, if omitted, it is an anonymous user
|
||||||
4: Add `:rw` after the path to indicate that the path has read and write permissions, otherwise the path has readonly permissions.
|
3. One rule can set multiple paths, separated by ","
|
||||||
|
4. Add `:rw` after the path to indicate that the path has read and write permissions, otherwise the path has readonly permissions.
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs -A -a admin:admin@/:rw
|
dufs -A -a admin:admin@/:rw
|
||||||
@@ -241,10 +243,33 @@ dufs -A -a user:pass@/dir1:rw,/dir2:rw,dir3
|
|||||||
`user` has all permissions for `/dir1/*` and `/dir2/*`, has readonly permissions for `/dir3/`.
|
`user` has all permissions for `/dir1/*` and `/dir2/*`, has readonly permissions for `/dir3/`.
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs -a admin:admin@/
|
dufs -A -a admin:admin@/
|
||||||
```
|
```
|
||||||
Since dufs only allows viewing/downloading, `admin` can only view/download files.
|
Since dufs only allows viewing/downloading, `admin` can only view/download files.
|
||||||
|
|
||||||
|
#### Hashed Password
|
||||||
|
|
||||||
|
DUFS supports the use of sha-512 hashed password.
|
||||||
|
|
||||||
|
Create hashed password
|
||||||
|
|
||||||
|
```
|
||||||
|
$ mkpasswd -m sha-512 -s
|
||||||
|
Password: 123456
|
||||||
|
$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/
|
||||||
|
```
|
||||||
|
|
||||||
|
Use hashed password
|
||||||
|
```
|
||||||
|
dufs -A -a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw'
|
||||||
|
```
|
||||||
|
|
||||||
|
Two important things for hashed passwords:
|
||||||
|
|
||||||
|
1. Dufs only supports SHA-512 hashed passwords, so ensure that the password string always starts with `$6$`.
|
||||||
|
2. Digest auth does not work with hashed passwords.
|
||||||
|
|
||||||
|
|
||||||
### Hide Paths
|
### Hide Paths
|
||||||
|
|
||||||
Dufs supports hiding paths from directory listings via option `--hidden <glob>,...`.
|
Dufs supports hiding paths from directory listings via option `--hidden <glob>,...`.
|
||||||
@@ -307,13 +332,13 @@ dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admi
|
|||||||
All options can be set using environment variables prefixed with `DUFS_`.
|
All options can be set using environment variables prefixed with `DUFS_`.
|
||||||
|
|
||||||
```
|
```
|
||||||
[ROOT_DIR] DUFS_ROOT_DIR=/dir
|
[serve-path] DUFS_SERVE_PATH=/dir
|
||||||
|
--config <path> DUFS_CONFIG=config.yaml
|
||||||
-b, --bind <addrs> DUFS_BIND=0.0.0.0
|
-b, --bind <addrs> DUFS_BIND=0.0.0.0
|
||||||
-p, --port <port> DUFS_PORT=5000
|
-p, --port <port> DUFS_PORT=5000
|
||||||
--path-prefix <path> DUFS_PATH_RREFIX=/path
|
--path-prefix <path> DUFS_PATH_PREFIX=/path
|
||||||
--hidden <value> DUFS_HIDDEN=*.log
|
--hidden <value> DUFS_HIDDEN=*.log
|
||||||
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
|
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
|
||||||
--auth-method <value> DUFS_AUTH_METHOD=basic
|
|
||||||
-A, --allow-all DUFS_ALLOW_ALL=true
|
-A, --allow-all DUFS_ALLOW_ALL=true
|
||||||
--allow-upload DUFS_ALLOW_UPLOAD=true
|
--allow-upload DUFS_ALLOW_UPLOAD=true
|
||||||
--allow-delete DUFS_ALLOW_DELETE=true
|
--allow-delete DUFS_ALLOW_DELETE=true
|
||||||
@@ -325,9 +350,44 @@ All options can be set using environment variables prefixed with `DUFS_`.
|
|||||||
--render-try-index DUFS_RENDER_TRY_INDEX=true
|
--render-try-index DUFS_RENDER_TRY_INDEX=true
|
||||||
--render-spa DUFS_RENDER_SPA=true
|
--render-spa DUFS_RENDER_SPA=true
|
||||||
--assets <path> DUFS_ASSETS=/assets
|
--assets <path> DUFS_ASSETS=/assets
|
||||||
|
--log-format <format> DUFS_LOG_FORMAT=""
|
||||||
--tls-cert <path> DUFS_TLS_CERT=cert.pem
|
--tls-cert <path> DUFS_TLS_CERT=cert.pem
|
||||||
--tls-key <path> DUFS_TLS_KEY=key.pem
|
--tls-key <path> DUFS_TLS_KEY=key.pem
|
||||||
--log-format <format> DUFS_LOG_FORMAT=""
|
```
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
You can specify and use the configuration file by selecting the option `--config <path-to-config.yaml>`.
|
||||||
|
|
||||||
|
The following are the configuration items:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
serve-path: '.'
|
||||||
|
bind:
|
||||||
|
- 192.168.8.10
|
||||||
|
port: 5000
|
||||||
|
path-prefix: /dufs
|
||||||
|
hidden:
|
||||||
|
- tmp
|
||||||
|
- '*.log'
|
||||||
|
- '*.lock'
|
||||||
|
auth:
|
||||||
|
- admin:admin@/:rw
|
||||||
|
- user:pass@/src:rw,/share
|
||||||
|
allow-all: false
|
||||||
|
allow-upload: true
|
||||||
|
allow-delete: true
|
||||||
|
allow-search: true
|
||||||
|
allow-symlink: true
|
||||||
|
allow-archive: true
|
||||||
|
enable-cors: true
|
||||||
|
render-index: true
|
||||||
|
render-try-index: true
|
||||||
|
render-spa: true
|
||||||
|
assets: ./assets/
|
||||||
|
log-format: '$remote_addr "$request" $status $http_user_agent'
|
||||||
|
tls-cert: tests/data/cert.pem
|
||||||
|
tls-key: tests/data/key_pkcs1.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customize UI
|
### Customize UI
|
||||||
@@ -343,7 +403,7 @@ Your assets folder must contains a `index.html` file.
|
|||||||
`index.html` can use the following placeholder variables to retrieve internal data.
|
`index.html` can use the following placeholder variables to retrieve internal data.
|
||||||
|
|
||||||
- `__INDEX_DATA__`: directory listing data
|
- `__INDEX_DATA__`: directory listing data
|
||||||
- `__ASSERTS_PREFIX__`: assets url prefix
|
- `__ASSETS_PREFIX__`: assets url prefix
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ body {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox2 {
|
.toolbox-right {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,15 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.not-editable {
|
.not-editable {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/x-icon" href="__ASSERTS_PREFIX__favicon.ico">
|
<link rel="icon" type="image/x-icon" href="__ASSETS_PREFIX__favicon.ico">
|
||||||
<link rel="stylesheet" href="__ASSERTS_PREFIX__index.css">
|
<link rel="stylesheet" href="__ASSETS_PREFIX__index.css">
|
||||||
<script>
|
<script>
|
||||||
DATA = __INDEX_DATA__
|
DATA = __INDEX_DATA__
|
||||||
</script>
|
</script>
|
||||||
<script src="__ASSERTS_PREFIX__index.js"></script>
|
<script src="__ASSETS_PREFIX__index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<input id="search" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
|
<input id="search" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
|
||||||
<input type="submit" hidden />
|
<input type="submit" hidden />
|
||||||
</form>
|
</form>
|
||||||
<div class="toolbox2">
|
<div class="toolbox-right">
|
||||||
<div class="login-btn hidden" title="Login for upload/move/delete/edit permissions">
|
<div class="login-btn hidden" title="Login for upload/move/delete/edit permissions">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd"
|
<path fill-rule="evenodd"
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
<path
|
<path
|
||||||
d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z" />
|
d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<span class="user-name"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="save-btn hidden" title="Save file">
|
<div class="save-btn hidden" title="Save file">
|
||||||
<svg viewBox="0 0 1024 1024" width="24" height="24">
|
<svg viewBox="0 0 1024 1024" width="24" height="24">
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ let $editor;
|
|||||||
* @type Element
|
* @type Element
|
||||||
*/
|
*/
|
||||||
let $userBtn;
|
let $userBtn;
|
||||||
|
/**
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
let $userName;
|
||||||
|
|
||||||
function ready() {
|
function ready() {
|
||||||
$pathsTable = document.querySelector(".paths-table")
|
$pathsTable = document.querySelector(".paths-table")
|
||||||
@@ -95,6 +99,7 @@ function ready() {
|
|||||||
$emptyFolder = document.querySelector(".empty-folder");
|
$emptyFolder = document.querySelector(".empty-folder");
|
||||||
$editor = document.querySelector(".editor");
|
$editor = document.querySelector(".editor");
|
||||||
$userBtn = document.querySelector(".user-btn");
|
$userBtn = document.querySelector(".user-btn");
|
||||||
|
$userName = document.querySelector(".user-name");
|
||||||
|
|
||||||
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
||||||
|
|
||||||
@@ -218,8 +223,11 @@ Uploader.runQueue = async () => {
|
|||||||
let uploader = Uploader.queues.shift();
|
let uploader = Uploader.queues.shift();
|
||||||
if (!Uploader.auth) {
|
if (!Uploader.auth) {
|
||||||
Uploader.auth = true;
|
Uploader.auth = true;
|
||||||
const success = await checkAuth(true);
|
try {
|
||||||
Uploader.auth = !!success;
|
await checkAuth()
|
||||||
|
} catch {
|
||||||
|
Uploader.auth = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
uploader.ajax();
|
uploader.ajax();
|
||||||
}
|
}
|
||||||
@@ -313,13 +321,13 @@ function renderPathsTableHead() {
|
|||||||
<tr>
|
<tr>
|
||||||
${headerItems.map(item => {
|
${headerItems.map(item => {
|
||||||
let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
|
let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
|
||||||
let order = "asc";
|
let order = "desc";
|
||||||
if (PARAMS.sort === item.name) {
|
if (PARAMS.sort === item.name) {
|
||||||
if (PARAMS.order === "asc") {
|
if (PARAMS.order === "desc") {
|
||||||
order = "desc";
|
order = "asc";
|
||||||
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
|
|
||||||
} else {
|
|
||||||
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
|
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
|
||||||
|
} else {
|
||||||
|
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString();
|
const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString();
|
||||||
@@ -435,11 +443,18 @@ function setupDropzone() {
|
|||||||
function setupAuth() {
|
function setupAuth() {
|
||||||
if (DATA.user) {
|
if (DATA.user) {
|
||||||
$userBtn.classList.remove("hidden");
|
$userBtn.classList.remove("hidden");
|
||||||
$userBtn.title = DATA.user;
|
$userName.textContent = DATA.user;
|
||||||
} else {
|
} else {
|
||||||
const $loginBtn = document.querySelector(".login-btn");
|
const $loginBtn = document.querySelector(".login-btn");
|
||||||
$loginBtn.classList.remove("hidden");
|
$loginBtn.classList.remove("hidden");
|
||||||
$loginBtn.addEventListener("click", () => checkAuth(true));
|
$loginBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await checkAuth()
|
||||||
|
location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,25 +666,15 @@ async function saveChange() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkAuth(alert = false) {
|
async function checkAuth() {
|
||||||
if (!DATA.auth) return;
|
if (!DATA.auth) return;
|
||||||
try {
|
const res = await fetch(baseUrl(), {
|
||||||
const res = await fetch(baseUrl(), {
|
method: "WRITEABLE",
|
||||||
method: "WRITEABLE",
|
});
|
||||||
});
|
await assertResOK(res);
|
||||||
await assertResOK(res);
|
document.querySelector(".login-btn").classList.add("hidden");
|
||||||
document.querySelector(".login-btn").classList.add("hidden");
|
$userBtn.classList.remove("hidden");
|
||||||
$userBtn.classList.remove("hidden");
|
$userName.textContent = "";
|
||||||
$userBtn.title = "";
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
let message = `Check auth, ${err.message}`;
|
|
||||||
if (alert) {
|
|
||||||
alert(message);
|
|
||||||
} else {
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -808,7 +813,7 @@ function encodedStr(rawStr) {
|
|||||||
|
|
||||||
async function assertResOK(res) {
|
async function assertResOK(res) {
|
||||||
if (!(res.status >= 200 && res.status < 300)) {
|
if (!(res.status >= 200 && res.status < 300)) {
|
||||||
throw new Error(await res.text())
|
throw new Error(await res.text() || `Invalid status ${res.status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
372
src/args.rs
372
src/args.rs
@@ -2,17 +2,13 @@ use anyhow::{bail, Context, Result};
|
|||||||
use clap::builder::PossibleValuesParser;
|
use clap::builder::PossibleValuesParser;
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use clap_complete::{generate, Generator, Shell};
|
use clap_complete::{generate, Generator, Shell};
|
||||||
#[cfg(feature = "tls")]
|
use serde::{Deserialize, Deserializer};
|
||||||
use rustls::{Certificate, PrivateKey};
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::auth::AccessControl;
|
use crate::auth::AccessControl;
|
||||||
use crate::auth::AuthMethod;
|
use crate::http_logger::HttpLogger;
|
||||||
use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT};
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
use crate::tls::{load_certs, load_private_key};
|
|
||||||
use crate::utils::encode_uri;
|
use crate::utils::encode_uri;
|
||||||
|
|
||||||
pub fn build_cli() -> Command {
|
pub fn build_cli() -> Command {
|
||||||
@@ -25,12 +21,20 @@ pub fn build_cli() -> Command {
|
|||||||
env!("CARGO_PKG_REPOSITORY")
|
env!("CARGO_PKG_REPOSITORY")
|
||||||
))
|
))
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("serve_path")
|
Arg::new("serve-path")
|
||||||
.env("DUFS_SERVE_PATH")
|
.env("DUFS_SERVE_PATH")
|
||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.default_value(".")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
.help("Specific path to serve"),
|
.help("Specific path to serve [default: .]"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("config")
|
||||||
|
.env("DUFS_CONFIG")
|
||||||
|
.hide_env(true)
|
||||||
|
.short('c')
|
||||||
|
.long("config")
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.help("Specify configuration file"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("bind")
|
Arg::new("bind")
|
||||||
@@ -49,9 +53,8 @@ pub fn build_cli() -> Command {
|
|||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.short('p')
|
.short('p')
|
||||||
.long("port")
|
.long("port")
|
||||||
.default_value("5000")
|
|
||||||
.value_parser(value_parser!(u16))
|
.value_parser(value_parser!(u16))
|
||||||
.help("Specify port to listen on")
|
.help("Specify port to listen on [default: 5000]")
|
||||||
.value_name("port"),
|
.value_name("port"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -67,7 +70,7 @@ pub fn build_cli() -> Command {
|
|||||||
.env("DUFS_HIDDEN")
|
.env("DUFS_HIDDEN")
|
||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.long("hidden")
|
.long("hidden")
|
||||||
.help("Hide paths from directory listings, separated by `,`")
|
.help("Hide paths from directory listings, e.g. tmp,*.log,*.lock")
|
||||||
.value_name("value"),
|
.value_name("value"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -76,13 +79,14 @@ pub fn build_cli() -> Command {
|
|||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.short('a')
|
.short('a')
|
||||||
.long("auth")
|
.long("auth")
|
||||||
.help("Add auth role")
|
.help("Add auth roles, e.g. user:pass@/dir1:rw,/dir2")
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
.value_delimiter('|')
|
.value_delimiter('|')
|
||||||
.value_name("rules"),
|
.value_name("rules"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("auth-method")
|
Arg::new("auth-method")
|
||||||
|
.hide(true)
|
||||||
.env("DUFS_AUTH_METHOD")
|
.env("DUFS_AUTH_METHOD")
|
||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.long("auth-method")
|
.long("auth-method")
|
||||||
@@ -177,9 +181,24 @@ pub fn build_cli() -> Command {
|
|||||||
.env("DUFS_ASSETS")
|
.env("DUFS_ASSETS")
|
||||||
.hide_env(true)
|
.hide_env(true)
|
||||||
.long("assets")
|
.long("assets")
|
||||||
.help("Use custom assets to override builtin assets")
|
.help("Set the path to the assets directory for overriding the built-in assets")
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("log-format")
|
||||||
|
.env("DUFS_LOG_FORMAT")
|
||||||
|
.hide_env(true)
|
||||||
|
.long("log-format")
|
||||||
|
.value_name("format")
|
||||||
|
.help("Customize http log format"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("completions")
|
||||||
|
.long("completions")
|
||||||
|
.value_name("shell")
|
||||||
|
.value_parser(value_parser!(Shell))
|
||||||
|
.help("Print shell completion script for <shell>"),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
@@ -203,38 +222,32 @@ pub fn build_cli() -> Command {
|
|||||||
.help("Path to the SSL/TLS certificate's private key"),
|
.help("Path to the SSL/TLS certificate's private key"),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.arg(
|
app
|
||||||
Arg::new("log-format")
|
|
||||||
.env("DUFS_LOG_FORMAT")
|
|
||||||
.hide_env(true)
|
|
||||||
.long("log-format")
|
|
||||||
.value_name("format")
|
|
||||||
.help("Customize http log format"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("completions")
|
|
||||||
.long("completions")
|
|
||||||
.value_name("shell")
|
|
||||||
.value_parser(value_parser!(Shell))
|
|
||||||
.help("Print shell completion script for <shell>"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
|
pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
|
||||||
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
|
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
#[serde(default = "default_serve_path")]
|
||||||
|
pub serve_path: PathBuf,
|
||||||
|
#[serde(deserialize_with = "deserialize_bind_addrs")]
|
||||||
|
#[serde(rename = "bind")]
|
||||||
pub addrs: Vec<BindAddr>,
|
pub addrs: Vec<BindAddr>,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub path: PathBuf,
|
#[serde(skip)]
|
||||||
pub path_is_file: bool,
|
pub path_is_file: bool,
|
||||||
pub path_prefix: String,
|
pub path_prefix: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub uri_prefix: String,
|
pub uri_prefix: String,
|
||||||
pub hidden: Vec<String>,
|
pub hidden: Vec<String>,
|
||||||
pub auth_method: AuthMethod,
|
#[serde(deserialize_with = "deserialize_access_control")]
|
||||||
pub auth: AccessControl,
|
pub auth: AccessControl,
|
||||||
|
pub allow_all: bool,
|
||||||
pub allow_upload: bool,
|
pub allow_upload: bool,
|
||||||
pub allow_delete: bool,
|
pub allow_delete: bool,
|
||||||
pub allow_search: bool,
|
pub allow_search: bool,
|
||||||
@@ -244,12 +257,12 @@ pub struct Args {
|
|||||||
pub render_spa: bool,
|
pub render_spa: bool,
|
||||||
pub render_try_index: bool,
|
pub render_try_index: bool,
|
||||||
pub enable_cors: bool,
|
pub enable_cors: bool,
|
||||||
pub assets_path: Option<PathBuf>,
|
pub assets: Option<PathBuf>,
|
||||||
pub log_http: LogHttp,
|
#[serde(deserialize_with = "deserialize_log_http")]
|
||||||
#[cfg(feature = "tls")]
|
#[serde(rename = "log-format")]
|
||||||
pub tls: Option<(Vec<Certificate>, PrivateKey)>,
|
pub http_logger: HttpLogger,
|
||||||
#[cfg(not(feature = "tls"))]
|
pub tls_cert: Option<PathBuf>,
|
||||||
pub tls: Option<()>,
|
pub tls_key: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@@ -258,95 +271,164 @@ impl Args {
|
|||||||
/// If a parsing error occurred, exit the process and print out informative
|
/// If a parsing error occurred, exit the process and print out informative
|
||||||
/// error message to user.
|
/// error message to user.
|
||||||
pub fn parse(matches: ArgMatches) -> Result<Args> {
|
pub fn parse(matches: ArgMatches) -> Result<Args> {
|
||||||
let port = *matches.get_one::<u16>("port").unwrap();
|
let mut args = Self {
|
||||||
let addrs = matches
|
serve_path: default_serve_path(),
|
||||||
.get_many::<String>("bind")
|
addrs: BindAddr::parse_addrs(&["0.0.0.0", "::"]).unwrap(),
|
||||||
.map(|bind| bind.map(|v| v.as_str()).collect())
|
port: 5000,
|
||||||
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
|
..Default::default()
|
||||||
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?;
|
};
|
||||||
let path = Args::parse_path(matches.get_one::<PathBuf>("serve_path").unwrap())?;
|
|
||||||
let path_is_file = path.metadata()?.is_file();
|
if let Some(config_path) = matches.get_one::<PathBuf>("config") {
|
||||||
let path_prefix = matches
|
let contents = std::fs::read_to_string(config_path)
|
||||||
.get_one::<String>("path-prefix")
|
.with_context(|| format!("Failed to read config at {}", config_path.display()))?;
|
||||||
.map(|v| v.trim_matches('/').to_owned())
|
args = serde_yaml::from_str(&contents)
|
||||||
.unwrap_or_default();
|
.with_context(|| format!("Failed to load config at {}", config_path.display()))?;
|
||||||
let uri_prefix = if path_prefix.is_empty() {
|
}
|
||||||
|
|
||||||
|
if let Some(path) = matches.get_one::<PathBuf>("serve-path") {
|
||||||
|
args.serve_path = path.clone()
|
||||||
|
}
|
||||||
|
args.serve_path = Self::sanitize_path(args.serve_path)?;
|
||||||
|
|
||||||
|
if let Some(port) = matches.get_one::<u16>("port") {
|
||||||
|
args.port = *port
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(addrs) = matches.get_many::<String>("bind") {
|
||||||
|
let addrs: Vec<_> = addrs.map(|v| v.as_str()).collect();
|
||||||
|
args.addrs = BindAddr::parse_addrs(&addrs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.path_is_file = args.serve_path.metadata()?.is_file();
|
||||||
|
if let Some(path_prefix) = matches.get_one::<String>("path-prefix") {
|
||||||
|
args.path_prefix = path_prefix.clone();
|
||||||
|
}
|
||||||
|
args.path_prefix = args.path_prefix.trim_matches('/').to_string();
|
||||||
|
|
||||||
|
args.uri_prefix = if args.path_prefix.is_empty() {
|
||||||
"/".to_owned()
|
"/".to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("/{}/", &encode_uri(&path_prefix))
|
format!("/{}/", &encode_uri(&args.path_prefix))
|
||||||
};
|
};
|
||||||
let hidden: Vec<String> = matches
|
|
||||||
|
if let Some(hidden) = matches
|
||||||
.get_one::<String>("hidden")
|
.get_one::<String>("hidden")
|
||||||
.map(|v| v.split(',').map(|x| x.to_string()).collect())
|
.map(|v| v.split(',').map(|x| x.to_string()).collect())
|
||||||
.unwrap_or_default();
|
{
|
||||||
let enable_cors = matches.get_flag("enable-cors");
|
args.hidden = hidden;
|
||||||
let auth: Vec<&str> = matches
|
}
|
||||||
.get_many::<String>("auth")
|
|
||||||
.map(|auth| auth.map(|v| v.as_str()).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let auth_method = match matches.get_one::<String>("auth-method").unwrap().as_str() {
|
|
||||||
"basic" => AuthMethod::Basic,
|
|
||||||
_ => AuthMethod::Digest,
|
|
||||||
};
|
|
||||||
let auth = AccessControl::new(&auth)?;
|
|
||||||
let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload");
|
|
||||||
let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete");
|
|
||||||
let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search");
|
|
||||||
let allow_symlink = matches.get_flag("allow-all") || matches.get_flag("allow-symlink");
|
|
||||||
let allow_archive = matches.get_flag("allow-all") || matches.get_flag("allow-archive");
|
|
||||||
let render_index = matches.get_flag("render-index");
|
|
||||||
let render_try_index = matches.get_flag("render-try-index");
|
|
||||||
let render_spa = matches.get_flag("render-spa");
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
let tls = match (
|
|
||||||
matches.get_one::<PathBuf>("tls-cert"),
|
|
||||||
matches.get_one::<PathBuf>("tls-key"),
|
|
||||||
) {
|
|
||||||
(Some(certs_file), Some(key_file)) => {
|
|
||||||
let certs = load_certs(certs_file)?;
|
|
||||||
let key = load_private_key(key_file)?;
|
|
||||||
Some((certs, key))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
#[cfg(not(feature = "tls"))]
|
|
||||||
let tls = None;
|
|
||||||
let log_http: LogHttp = matches
|
|
||||||
.get_one::<String>("log-format")
|
|
||||||
.map(|v| v.as_str())
|
|
||||||
.unwrap_or(DEFAULT_LOG_FORMAT)
|
|
||||||
.parse()?;
|
|
||||||
let assets_path = match matches.get_one::<PathBuf>("assets") {
|
|
||||||
Some(v) => Some(Args::parse_assets_path(v)?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Args {
|
if !args.enable_cors {
|
||||||
addrs,
|
args.enable_cors = matches.get_flag("enable-cors");
|
||||||
port,
|
}
|
||||||
path,
|
|
||||||
path_is_file,
|
if let Some(rules) = matches.get_many::<String>("auth") {
|
||||||
path_prefix,
|
let rules: Vec<_> = rules.map(|v| v.as_str()).collect();
|
||||||
uri_prefix,
|
args.auth = AccessControl::new(&rules)?;
|
||||||
hidden,
|
}
|
||||||
auth_method,
|
|
||||||
auth,
|
if !args.allow_all {
|
||||||
enable_cors,
|
args.allow_all = matches.get_flag("allow-all");
|
||||||
allow_delete,
|
}
|
||||||
allow_upload,
|
|
||||||
allow_search,
|
let allow_all = args.allow_all;
|
||||||
allow_symlink,
|
|
||||||
allow_archive,
|
if !args.allow_upload {
|
||||||
render_index,
|
args.allow_upload = allow_all || matches.get_flag("allow-upload");
|
||||||
render_try_index,
|
}
|
||||||
render_spa,
|
if !args.allow_delete {
|
||||||
tls,
|
args.allow_delete = allow_all || matches.get_flag("allow-delete");
|
||||||
log_http,
|
}
|
||||||
assets_path,
|
if !args.allow_search {
|
||||||
})
|
args.allow_search = allow_all || matches.get_flag("allow-search");
|
||||||
|
}
|
||||||
|
if !args.allow_symlink {
|
||||||
|
args.allow_symlink = allow_all || matches.get_flag("allow-symlink");
|
||||||
|
}
|
||||||
|
if !args.allow_archive {
|
||||||
|
args.allow_archive = allow_all || matches.get_flag("allow-archive");
|
||||||
|
}
|
||||||
|
if !args.render_index {
|
||||||
|
args.render_index = matches.get_flag("render-index");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.render_try_index {
|
||||||
|
args.render_try_index = matches.get_flag("render-try-index");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.render_spa {
|
||||||
|
args.render_spa = matches.get_flag("render-spa");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(log_format) = matches.get_one::<String>("log-format") {
|
||||||
|
args.http_logger = log_format.parse()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(assets_path) = matches.get_one::<PathBuf>("assets") {
|
||||||
|
args.assets = Some(assets_path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(assets_path) = &args.assets {
|
||||||
|
args.assets = Some(Args::sanitize_assets_path(assets_path)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
{
|
||||||
|
if let Some(tls_cert) = matches.get_one::<PathBuf>("tls-cert") {
|
||||||
|
args.tls_cert = Some(tls_cert.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tls_key) = matches.get_one::<PathBuf>("tls-key") {
|
||||||
|
args.tls_key = Some(tls_key.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
match (&args.tls_cert, &args.tls_key) {
|
||||||
|
(Some(_), Some(_)) => {}
|
||||||
|
(Some(_), _) => bail!("No tls-key set"),
|
||||||
|
(_, Some(_)) => bail!("No tls-cert set"),
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tls"))]
|
||||||
|
{
|
||||||
|
args.tls_cert = None;
|
||||||
|
args.tls_key = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
|
fn sanitize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if !path.exists() {
|
||||||
|
bail!("Path `{}` doesn't exist", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
env::current_dir()
|
||||||
|
.and_then(|mut p| {
|
||||||
|
p.push(path); // If path is absolute, it replaces the current path.
|
||||||
|
std::fs::canonicalize(p)
|
||||||
|
})
|
||||||
|
.with_context(|| format!("Failed to access path `{}`", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||||
|
let path = Self::sanitize_path(path)?;
|
||||||
|
if !path.join("index.html").exists() {
|
||||||
|
bail!("Path `{}` doesn't contains index.html", path.display());
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum BindAddr {
|
||||||
|
Address(IpAddr),
|
||||||
|
Path(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindAddr {
|
||||||
|
fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
|
||||||
let mut bind_addrs = vec![];
|
let mut bind_addrs = vec![];
|
||||||
let mut invalid_addrs = vec![];
|
let mut invalid_addrs = vec![];
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
@@ -368,32 +450,32 @@ impl Args {
|
|||||||
}
|
}
|
||||||
Ok(bind_addrs)
|
Ok(bind_addrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.exists() {
|
|
||||||
bail!("Path `{}` doesn't exist", path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
env::current_dir()
|
|
||||||
.and_then(|mut p| {
|
|
||||||
p.push(path); // If path is absolute, it replaces the current path.
|
|
||||||
std::fs::canonicalize(p)
|
|
||||||
})
|
|
||||||
.with_context(|| format!("Failed to access path `{}`", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
|
||||||
let path = Self::parse_path(path)?;
|
|
||||||
if !path.join("index.html").exists() {
|
|
||||||
bail!("Path `{}` doesn't contains index.html", path.display());
|
|
||||||
}
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, D::Error>
|
||||||
pub enum BindAddr {
|
where
|
||||||
Address(IpAddr),
|
D: Deserializer<'de>,
|
||||||
Path(PathBuf),
|
{
|
||||||
|
let addrs: Vec<&str> = Vec::deserialize(deserializer)?;
|
||||||
|
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_access_control<'de, D>(deserializer: D) -> Result<AccessControl, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let rules: Vec<&str> = Vec::deserialize(deserializer)?;
|
||||||
|
AccessControl::new(&rules).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_log_http<'de, D>(deserializer: D) -> Result<HttpLogger, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
value.parse().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_serve_path() -> PathBuf {
|
||||||
|
PathBuf::from(".")
|
||||||
}
|
}
|
||||||
|
|||||||
298
src/auth.rs
298
src/auth.rs
@@ -11,10 +11,10 @@ use std::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::utils::unix_now;
|
use crate::{args::Args, utils::unix_now};
|
||||||
|
|
||||||
const REALM: &str = "DUFS";
|
const REALM: &str = "DUFS";
|
||||||
const DIGEST_AUTH_TIMEOUT: u32 = 86400;
|
const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref NONCESTARTHASH: Context = {
|
static ref NONCESTARTHASH: Context = {
|
||||||
@@ -25,21 +25,29 @@ lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct AccessControl {
|
pub struct AccessControl {
|
||||||
|
use_hashed_password: bool,
|
||||||
users: IndexMap<String, (String, AccessPaths)>,
|
users: IndexMap<String, (String, AccessPaths)>,
|
||||||
anony: Option<AccessPaths>,
|
anony: Option<AccessPaths>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for AccessControl {
|
||||||
|
fn default() -> Self {
|
||||||
|
AccessControl {
|
||||||
|
use_hashed_password: false,
|
||||||
|
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
|
||||||
|
users: IndexMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AccessControl {
|
impl AccessControl {
|
||||||
pub fn new(raw_rules: &[&str]) -> Result<Self> {
|
pub fn new(raw_rules: &[&str]) -> Result<Self> {
|
||||||
if raw_rules.is_empty() {
|
if raw_rules.is_empty() {
|
||||||
return Ok(AccessControl {
|
return Ok(Default::default());
|
||||||
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
|
|
||||||
users: IndexMap::new(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
let mut use_hashed_password = false;
|
||||||
let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
|
let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
|
||||||
let mut anony = None;
|
let mut anony = None;
|
||||||
let mut anony_paths = vec![];
|
let mut anony_paths = vec![];
|
||||||
@@ -67,6 +75,9 @@ impl AccessControl {
|
|||||||
if user.is_empty() || pass.is_empty() {
|
if user.is_empty() || pass.is_empty() {
|
||||||
return Err(create_err(rule));
|
return Err(create_err(rule));
|
||||||
}
|
}
|
||||||
|
if pass.starts_with("$6$") {
|
||||||
|
use_hashed_password = true;
|
||||||
|
}
|
||||||
users.insert(user.to_string(), (pass.to_string(), paths));
|
users.insert(user.to_string(), (pass.to_string(), paths));
|
||||||
} else {
|
} else {
|
||||||
return Err(create_err(rule));
|
return Err(create_err(rule));
|
||||||
@@ -77,11 +88,15 @@ impl AccessControl {
|
|||||||
paths.add(path, perm)
|
paths.add(path, perm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self { users, anony })
|
Ok(Self {
|
||||||
|
use_hashed_password,
|
||||||
|
users,
|
||||||
|
anony,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid(&self) -> bool {
|
pub fn exist(&self) -> bool {
|
||||||
!self.users.is_empty() || self.anony.is_some()
|
!self.users.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guard(
|
pub fn guard(
|
||||||
@@ -89,18 +104,14 @@ impl AccessControl {
|
|||||||
path: &str,
|
path: &str,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
authorization: Option<&HeaderValue>,
|
authorization: Option<&HeaderValue>,
|
||||||
auth_method: AuthMethod,
|
|
||||||
) -> (Option<String>, Option<AccessPaths>) {
|
) -> (Option<String>, Option<AccessPaths>) {
|
||||||
if let Some(authorization) = authorization {
|
if let Some(authorization) = authorization {
|
||||||
if let Some(user) = auth_method.get_user(authorization) {
|
if let Some(user) = get_auth_user(authorization) {
|
||||||
if let Some((pass, paths)) = self.users.get(&user) {
|
if let Some((pass, paths)) = self.users.get(&user) {
|
||||||
if method == Method::OPTIONS {
|
if method == Method::OPTIONS {
|
||||||
return (Some(user), Some(AccessPaths::new(AccessPerm::ReadOnly)));
|
return (Some(user), Some(AccessPaths::new(AccessPerm::ReadOnly)));
|
||||||
}
|
}
|
||||||
if auth_method
|
if check_auth(authorization, method.as_str(), &user, pass).is_some() {
|
||||||
.check(authorization, method.as_str(), &user, pass)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return (Some(user), paths.find(path, !is_readonly_method(method)));
|
return (Some(user), paths.find(path, !is_readonly_method(method)));
|
||||||
} else {
|
} else {
|
||||||
return (None, None);
|
return (None, None);
|
||||||
@@ -229,8 +240,8 @@ impl AccessPaths {
|
|||||||
pub enum AccessPerm {
|
pub enum AccessPerm {
|
||||||
#[default]
|
#[default]
|
||||||
IndexOnly,
|
IndexOnly,
|
||||||
ReadWrite,
|
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
|
ReadWrite,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessPerm {
|
impl AccessPerm {
|
||||||
@@ -243,151 +254,127 @@ impl AccessPerm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_readonly_method(method: &Method) -> bool {
|
pub fn www_authenticate(args: &Args) -> Result<HeaderValue> {
|
||||||
method == Method::GET
|
let value = if args.auth.use_hashed_password {
|
||||||
|| method == Method::OPTIONS
|
format!("Basic realm=\"{}\"", REALM)
|
||||||
|| method == Method::HEAD
|
} else {
|
||||||
|| method.as_str() == "PROPFIND"
|
let nonce = create_nonce()?;
|
||||||
|
format!(
|
||||||
|
"Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"",
|
||||||
|
REALM, nonce, REALM
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(HeaderValue::from_str(&value)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub fn get_auth_user(authorization: &HeaderValue) -> Option<String> {
|
||||||
pub enum AuthMethod {
|
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
||||||
Basic,
|
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
|
||||||
Digest,
|
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
||||||
|
Some(parts[0].to_string())
|
||||||
|
} else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
|
||||||
|
let digest_map = to_headermap(value).ok()?;
|
||||||
|
let username = digest_map.get(b"username".as_ref())?;
|
||||||
|
std::str::from_utf8(username).map(|v| v.to_string()).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthMethod {
|
pub fn check_auth(
|
||||||
pub fn www_auth(&self, stale: bool) -> Result<String> {
|
authorization: &HeaderValue,
|
||||||
match self {
|
method: &str,
|
||||||
AuthMethod::Basic => Ok(format!("Basic realm=\"{REALM}\"")),
|
auth_user: &str,
|
||||||
AuthMethod::Digest => {
|
auth_pass: &str,
|
||||||
let str_stale = if stale { "stale=true," } else { "" };
|
) -> Option<()> {
|
||||||
Ok(format!(
|
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
||||||
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\"",
|
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
|
||||||
REALM,
|
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
||||||
create_nonce()?,
|
|
||||||
str_stale
|
if parts[0] != auth_user {
|
||||||
))
|
return None;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_user(&self, authorization: &HeaderValue) -> Option<String> {
|
if auth_pass.starts_with("$6$") {
|
||||||
match self {
|
if let Ok(()) = sha_crypt::sha512_check(parts[1], auth_pass) {
|
||||||
AuthMethod::Basic => {
|
return Some(());
|
||||||
let value: Vec<u8> = general_purpose::STANDARD
|
|
||||||
.decode(strip_prefix(authorization.as_bytes(), b"Basic ")?)
|
|
||||||
.ok()?;
|
|
||||||
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
|
||||||
Some(parts[0].to_string())
|
|
||||||
}
|
|
||||||
AuthMethod::Digest => {
|
|
||||||
let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
|
|
||||||
let digest_map = to_headermap(digest_value).ok()?;
|
|
||||||
digest_map
|
|
||||||
.get(b"username".as_ref())
|
|
||||||
.and_then(|b| std::str::from_utf8(b).ok())
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
}
|
}
|
||||||
|
} else if parts[1] == auth_pass {
|
||||||
|
return Some(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn check(
|
None
|
||||||
&self,
|
} else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
|
||||||
authorization: &HeaderValue,
|
let digest_map = to_headermap(value).ok()?;
|
||||||
method: &str,
|
if let (Some(username), Some(nonce), Some(user_response)) = (
|
||||||
auth_user: &str,
|
digest_map
|
||||||
auth_pass: &str,
|
.get(b"username".as_ref())
|
||||||
) -> Option<()> {
|
.and_then(|b| std::str::from_utf8(b).ok()),
|
||||||
match self {
|
digest_map.get(b"nonce".as_ref()),
|
||||||
AuthMethod::Basic => {
|
digest_map.get(b"response".as_ref()),
|
||||||
let basic_value: Vec<u8> = general_purpose::STANDARD
|
) {
|
||||||
.decode(strip_prefix(authorization.as_bytes(), b"Basic ")?)
|
match validate_nonce(nonce) {
|
||||||
.ok()?;
|
Ok(true) => {}
|
||||||
let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect();
|
_ => return None,
|
||||||
|
}
|
||||||
if parts[0] != auth_user {
|
if auth_user != username {
|
||||||
return None;
|
return None;
|
||||||
}
|
|
||||||
|
|
||||||
if parts[1] == auth_pass {
|
|
||||||
return Some(());
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
AuthMethod::Digest => {
|
|
||||||
let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
|
|
||||||
let digest_map = to_headermap(digest_value).ok()?;
|
|
||||||
if let (Some(username), Some(nonce), Some(user_response)) = (
|
|
||||||
digest_map
|
|
||||||
.get(b"username".as_ref())
|
|
||||||
.and_then(|b| std::str::from_utf8(b).ok()),
|
|
||||||
digest_map.get(b"nonce".as_ref()),
|
|
||||||
digest_map.get(b"response".as_ref()),
|
|
||||||
) {
|
|
||||||
match validate_nonce(nonce) {
|
|
||||||
Ok(true) => {}
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
if auth_user != username {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut h = Context::new();
|
let mut h = Context::new();
|
||||||
h.consume(format!("{}:{}:{}", auth_user, REALM, auth_pass).as_bytes());
|
h.consume(format!("{}:{}:{}", auth_user, REALM, auth_pass).as_bytes());
|
||||||
let auth_pass = format!("{:x}", h.compute());
|
let auth_pass = format!("{:x}", h.compute());
|
||||||
|
|
||||||
let mut ha = Context::new();
|
let mut ha = Context::new();
|
||||||
ha.consume(method);
|
ha.consume(method);
|
||||||
ha.consume(b":");
|
ha.consume(b":");
|
||||||
if let Some(uri) = digest_map.get(b"uri".as_ref()) {
|
if let Some(uri) = digest_map.get(b"uri".as_ref()) {
|
||||||
ha.consume(uri);
|
ha.consume(uri);
|
||||||
}
|
}
|
||||||
let ha = format!("{:x}", ha.compute());
|
let ha = format!("{:x}", ha.compute());
|
||||||
let mut correct_response = None;
|
let mut correct_response = None;
|
||||||
if let Some(qop) = digest_map.get(b"qop".as_ref()) {
|
if let Some(qop) = digest_map.get(b"qop".as_ref()) {
|
||||||
if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
|
if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
|
||||||
correct_response = Some({
|
correct_response = Some({
|
||||||
let mut c = Context::new();
|
let mut c = Context::new();
|
||||||
c.consume(&auth_pass);
|
c.consume(&auth_pass);
|
||||||
c.consume(b":");
|
c.consume(b":");
|
||||||
c.consume(nonce);
|
c.consume(nonce);
|
||||||
c.consume(b":");
|
c.consume(b":");
|
||||||
if let Some(nc) = digest_map.get(b"nc".as_ref()) {
|
if let Some(nc) = digest_map.get(b"nc".as_ref()) {
|
||||||
c.consume(nc);
|
c.consume(nc);
|
||||||
}
|
|
||||||
c.consume(b":");
|
|
||||||
if let Some(cnonce) = digest_map.get(b"cnonce".as_ref()) {
|
|
||||||
c.consume(cnonce);
|
|
||||||
}
|
|
||||||
c.consume(b":");
|
|
||||||
c.consume(qop);
|
|
||||||
c.consume(b":");
|
|
||||||
c.consume(&*ha);
|
|
||||||
format!("{:x}", c.compute())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
c.consume(b":");
|
||||||
let correct_response = match correct_response {
|
if let Some(cnonce) = digest_map.get(b"cnonce".as_ref()) {
|
||||||
Some(r) => r,
|
c.consume(cnonce);
|
||||||
None => {
|
|
||||||
let mut c = Context::new();
|
|
||||||
c.consume(&auth_pass);
|
|
||||||
c.consume(b":");
|
|
||||||
c.consume(nonce);
|
|
||||||
c.consume(b":");
|
|
||||||
c.consume(&*ha);
|
|
||||||
format!("{:x}", c.compute())
|
|
||||||
}
|
}
|
||||||
};
|
c.consume(b":");
|
||||||
if correct_response.as_bytes() == *user_response {
|
c.consume(qop);
|
||||||
return Some(());
|
c.consume(b":");
|
||||||
}
|
c.consume(&*ha);
|
||||||
|
format!("{:x}", c.compute())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None
|
}
|
||||||
|
let correct_response = match correct_response {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
let mut c = Context::new();
|
||||||
|
c.consume(&auth_pass);
|
||||||
|
c.consume(b":");
|
||||||
|
c.consume(nonce);
|
||||||
|
c.consume(b":");
|
||||||
|
c.consume(&*ha);
|
||||||
|
format!("{:x}", c.compute())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if correct_response.as_bytes() == *user_response {
|
||||||
|
return Some(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +406,13 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
|||||||
bail!("invalid nonce");
|
bail!("invalid nonce");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_readonly_method(method: &Method) -> bool {
|
||||||
|
method == Method::GET
|
||||||
|
|| method == Method::OPTIONS
|
||||||
|
|| method == Method::HEAD
|
||||||
|
|| method.as_str() == "PROPFIND"
|
||||||
|
}
|
||||||
|
|
||||||
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
||||||
let l = prefix.len();
|
let l = prefix.len();
|
||||||
if search.len() < l {
|
if search.len() < l {
|
||||||
@@ -519,4 +513,16 @@ mod tests {
|
|||||||
assert_eq!(paths.find("dir2", true), None);
|
assert_eq!(paths.find("dir2", true), None);
|
||||||
assert!(paths.find("dir1/file", true).is_some());
|
assert!(paths.find("dir1/file", true).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_access_paths_perm() {
|
||||||
|
let mut paths = AccessPaths::default();
|
||||||
|
assert_eq!(paths.perm(), AccessPerm::IndexOnly);
|
||||||
|
paths.set_perm(AccessPerm::ReadOnly);
|
||||||
|
assert_eq!(paths.perm(), AccessPerm::ReadOnly);
|
||||||
|
paths.set_perm(AccessPerm::ReadWrite);
|
||||||
|
assert_eq!(paths.perm(), AccessPerm::ReadWrite);
|
||||||
|
paths.set_perm(AccessPerm::ReadOnly);
|
||||||
|
assert_eq!(paths.perm(), AccessPerm::ReadWrite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use crate::{args::Args, server::Request};
|
use crate::{auth::get_auth_user, server::Request};
|
||||||
|
|
||||||
pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
|
pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LogHttp {
|
pub struct HttpLogger {
|
||||||
elements: Vec<LogElement>,
|
elements: Vec<LogElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for HttpLogger {
|
||||||
|
fn default() -> Self {
|
||||||
|
DEFAULT_LOG_FORMAT.parse().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum LogElement {
|
enum LogElement {
|
||||||
Variable(String),
|
Variable(String),
|
||||||
@@ -16,8 +22,8 @@ enum LogElement {
|
|||||||
Literal(String),
|
Literal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogHttp {
|
impl HttpLogger {
|
||||||
pub fn data(&self, req: &Request, args: &Arc<Args>) -> HashMap<String, String> {
|
pub fn data(&self, req: &Request) -> HashMap<String, String> {
|
||||||
let mut data = HashMap::default();
|
let mut data = HashMap::default();
|
||||||
for element in self.elements.iter() {
|
for element in self.elements.iter() {
|
||||||
match element {
|
match element {
|
||||||
@@ -26,10 +32,8 @@ impl LogHttp {
|
|||||||
data.insert(name.to_string(), format!("{} {}", req.method(), req.uri()));
|
data.insert(name.to_string(), format!("{} {}", req.method(), req.uri()));
|
||||||
}
|
}
|
||||||
"remote_user" => {
|
"remote_user" => {
|
||||||
if let Some(user) = req
|
if let Some(user) =
|
||||||
.headers()
|
req.headers().get("authorization").and_then(get_auth_user)
|
||||||
.get("authorization")
|
|
||||||
.and_then(|v| args.auth_method.get_user(v))
|
|
||||||
{
|
{
|
||||||
data.insert(name.to_string(), user);
|
data.insert(name.to_string(), user);
|
||||||
}
|
}
|
||||||
@@ -66,7 +70,7 @@ impl LogHttp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for LogHttp {
|
impl FromStr for HttpLogger {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let mut elements = vec![];
|
let mut elements = vec![];
|
||||||
26
src/main.rs
26
src/main.rs
@@ -1,6 +1,6 @@
|
|||||||
mod args;
|
mod args;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod log_http;
|
mod http_logger;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod server;
|
mod server;
|
||||||
mod streamer;
|
mod streamer;
|
||||||
@@ -16,7 +16,7 @@ extern crate log;
|
|||||||
use crate::args::{build_cli, print_completions, Args};
|
use crate::args::{build_cli, print_completions, Args};
|
||||||
use crate::server::{Request, Server};
|
use crate::server::{Request, Server};
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
use crate::tls::{TlsAcceptor, TlsStream};
|
use crate::tls::{load_certs, load_private_key, TlsAcceptor, TlsStream};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener};
|
use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener};
|
||||||
@@ -88,9 +88,12 @@ fn serve(
|
|||||||
BindAddr::Address(ip) => {
|
BindAddr::Address(ip) => {
|
||||||
let incoming = create_addr_incoming(SocketAddr::new(*ip, port))
|
let incoming = create_addr_incoming(SocketAddr::new(*ip, port))
|
||||||
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
|
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
|
||||||
match args.tls.as_ref() {
|
|
||||||
|
match (&args.tls_cert, &args.tls_key) {
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
Some((certs, key)) => {
|
(Some(cert_file), Some(key_file)) => {
|
||||||
|
let certs = load_certs(cert_file)?;
|
||||||
|
let key = load_private_key(key_file)?;
|
||||||
let config = ServerConfig::builder()
|
let config = ServerConfig::builder()
|
||||||
.with_safe_defaults()
|
.with_safe_defaults()
|
||||||
.with_no_client_auth()
|
.with_no_client_auth()
|
||||||
@@ -105,11 +108,7 @@ fn serve(
|
|||||||
tokio::spawn(hyper::Server::builder(accepter).serve(new_service));
|
tokio::spawn(hyper::Server::builder(accepter).serve(new_service));
|
||||||
handles.push(server);
|
handles.push(server);
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "tls"))]
|
(None, None) => {
|
||||||
Some(_) => {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let new_service = make_service_fn(move |socket: &AddrStream| {
|
let new_service = make_service_fn(move |socket: &AddrStream| {
|
||||||
let remote_addr = socket.remote_addr();
|
let remote_addr = socket.remote_addr();
|
||||||
serve_func(Some(remote_addr))
|
serve_func(Some(remote_addr))
|
||||||
@@ -118,6 +117,9 @@ fn serve(
|
|||||||
tokio::spawn(hyper::Server::builder(incoming).serve(new_service));
|
tokio::spawn(hyper::Server::builder(incoming).serve(new_service));
|
||||||
handles.push(server);
|
handles.push(server);
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
BindAddr::Path(path) => {
|
BindAddr::Path(path) => {
|
||||||
@@ -195,7 +197,11 @@ fn print_listening(args: Arc<Args>) -> Result<()> {
|
|||||||
IpAddr::V4(_) => format!("{}:{}", addr, args.port),
|
IpAddr::V4(_) => format!("{}:{}", addr, args.port),
|
||||||
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
|
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
|
||||||
};
|
};
|
||||||
let protocol = if args.tls.is_some() { "https" } else { "http" };
|
let protocol = if args.tls_cert.is_some() {
|
||||||
|
"https"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
};
|
||||||
format!("{}://{}{}", protocol, addr, args.uri_prefix)
|
format!("{}://{}{}", protocol, addr, args.uri_prefix)
|
||||||
}
|
}
|
||||||
BindAddr::Path(path) => path.display().to_string(),
|
BindAddr::Path(path) => path.display().to_string(),
|
||||||
|
|||||||
200
src/server.rs
200
src/server.rs
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use crate::auth::AccessPaths;
|
use crate::auth::{www_authenticate, AccessPaths, AccessPerm};
|
||||||
use crate::streamer::Streamer;
|
use crate::streamer::Streamer;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name,
|
decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, try_get_file_name,
|
||||||
@@ -26,12 +26,13 @@ use hyper::header::{
|
|||||||
use hyper::{Body, Method, StatusCode, Uri};
|
use hyper::{Body, Method, StatusCode, Uri};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::Metadata;
|
use std::fs::Metadata;
|
||||||
use std::io::SeekFrom;
|
use std::io::SeekFrom;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{self, AtomicBool};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
@@ -70,13 +71,13 @@ impl Server {
|
|||||||
encode_uri(&format!(
|
encode_uri(&format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
&args.uri_prefix,
|
&args.uri_prefix,
|
||||||
get_file_name(&args.path)
|
get_file_name(&args.serve_path)
|
||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
let html = match args.assets_path.as_ref() {
|
let html = match args.assets.as_ref() {
|
||||||
Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
|
Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
|
||||||
None => Cow::Borrowed(INDEX_HTML),
|
None => Cow::Borrowed(INDEX_HTML),
|
||||||
};
|
};
|
||||||
@@ -95,9 +96,9 @@ impl Server {
|
|||||||
addr: Option<SocketAddr>,
|
addr: Option<SocketAddr>,
|
||||||
) -> Result<Response, hyper::Error> {
|
) -> Result<Response, hyper::Error> {
|
||||||
let uri = req.uri().clone();
|
let uri = req.uri().clone();
|
||||||
let assets_prefix = self.assets_prefix.clone();
|
let assets_prefix = &self.assets_prefix;
|
||||||
let enable_cors = self.args.enable_cors;
|
let enable_cors = self.args.enable_cors;
|
||||||
let mut http_log_data = self.args.log_http.data(&req, &self.args);
|
let mut http_log_data = self.args.http_logger.data(&req);
|
||||||
if let Some(addr) = addr {
|
if let Some(addr) = addr {
|
||||||
http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
|
http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
|
||||||
}
|
}
|
||||||
@@ -105,8 +106,8 @@ impl Server {
|
|||||||
let mut res = match self.clone().handle(req).await {
|
let mut res = match self.clone().handle(req).await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
http_log_data.insert("status".to_string(), res.status().as_u16().to_string());
|
http_log_data.insert("status".to_string(), res.status().as_u16().to_string());
|
||||||
if !uri.path().starts_with(&assets_prefix) {
|
if !uri.path().starts_with(assets_prefix) {
|
||||||
self.args.log_http.log(&http_log_data, None);
|
self.args.http_logger.log(&http_log_data, None);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ impl Server {
|
|||||||
*res.status_mut() = status;
|
*res.status_mut() = status;
|
||||||
http_log_data.insert("status".to_string(), status.as_u16().to_string());
|
http_log_data.insert("status".to_string(), status.as_u16().to_string());
|
||||||
self.args
|
self.args
|
||||||
.log_http
|
.http_logger
|
||||||
.log(&http_log_data, Some(err.to_string()));
|
.log(&http_log_data, Some(err.to_string()));
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -148,12 +149,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let guard = self.args.auth.guard(
|
let guard = self.args.auth.guard(&relative_path, &method, authorization);
|
||||||
&relative_path,
|
|
||||||
&method,
|
|
||||||
authorization,
|
|
||||||
self.args.auth_method.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (user, access_paths) = match guard {
|
let (user, access_paths) = match guard {
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
@@ -184,7 +180,7 @@ impl Server {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|v| v.as_str() == req_path)
|
.any(|v| v.as_str() == req_path)
|
||||||
{
|
{
|
||||||
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
|
self.handle_send_file(&self.args.serve_path, headers, head_only, &mut res)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
status_not_found(&mut res);
|
status_not_found(&mut res);
|
||||||
@@ -340,6 +336,12 @@ impl Server {
|
|||||||
method => match method.as_str() {
|
method => match method.as_str() {
|
||||||
"PROPFIND" => {
|
"PROPFIND" => {
|
||||||
if is_dir {
|
if is_dir {
|
||||||
|
let access_paths = if access_paths.perm().indexonly() {
|
||||||
|
// see https://github.com/sigoden/dufs/issues/229
|
||||||
|
AccessPaths::new(AccessPerm::ReadOnly)
|
||||||
|
} else {
|
||||||
|
access_paths
|
||||||
|
};
|
||||||
self.handle_propfind_dir(path, headers, access_paths, &mut res)
|
self.handle_propfind_dir(path, headers, access_paths, &mut res)
|
||||||
.await?;
|
.await?;
|
||||||
} else if is_file {
|
} else if is_file {
|
||||||
@@ -425,7 +427,12 @@ impl Server {
|
|||||||
|
|
||||||
futures::pin_mut!(body_reader);
|
futures::pin_mut!(body_reader);
|
||||||
|
|
||||||
io::copy(&mut body_reader, &mut file).await?;
|
let ret = io::copy(&mut body_reader, &mut file).await;
|
||||||
|
if ret.is_err() {
|
||||||
|
tokio::fs::remove_file(&path).await?;
|
||||||
|
|
||||||
|
ret?;
|
||||||
|
}
|
||||||
|
|
||||||
*res.status_mut() = StatusCode::CREATED;
|
*res.status_mut() = StatusCode::CREATED;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -453,7 +460,7 @@ impl Server {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut paths = vec![];
|
let mut paths = vec![];
|
||||||
if exist {
|
if exist {
|
||||||
paths = match self.list_dir(path, path, access_paths).await {
|
paths = match self.list_dir(path, path, access_paths.clone()).await {
|
||||||
Ok(paths) => paths,
|
Ok(paths) => paths,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
status_forbid(res);
|
status_forbid(res);
|
||||||
@@ -461,7 +468,16 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.send_index(path, paths, exist, query_params, head_only, user, res)
|
self.send_index(
|
||||||
|
path,
|
||||||
|
paths,
|
||||||
|
exist,
|
||||||
|
query_params,
|
||||||
|
head_only,
|
||||||
|
user,
|
||||||
|
access_paths,
|
||||||
|
res,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_search_dir(
|
async fn handle_search_dir(
|
||||||
@@ -483,12 +499,14 @@ impl Server {
|
|||||||
let hidden = Arc::new(self.args.hidden.to_vec());
|
let hidden = Arc::new(self.args.hidden.to_vec());
|
||||||
let hidden = hidden.clone();
|
let hidden = hidden.clone();
|
||||||
let running = self.running.clone();
|
let running = self.running.clone();
|
||||||
|
let access_paths = access_paths.clone();
|
||||||
let search_paths = tokio::task::spawn_blocking(move || {
|
let search_paths = tokio::task::spawn_blocking(move || {
|
||||||
let mut paths: Vec<PathBuf> = vec![];
|
let mut paths: Vec<PathBuf> = vec![];
|
||||||
for dir in access_paths.leaf_paths(&path_buf) {
|
for dir in access_paths.leaf_paths(&path_buf) {
|
||||||
let mut it = WalkDir::new(&dir).into_iter();
|
let mut it = WalkDir::new(&dir).into_iter();
|
||||||
|
it.next();
|
||||||
while let Some(Ok(entry)) = it.next() {
|
while let Some(Ok(entry)) = it.next() {
|
||||||
if !running.load(Ordering::SeqCst) {
|
if !running.load(atomic::Ordering::SeqCst) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
@@ -526,7 +544,16 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.send_index(path, paths, true, query_params, head_only, user, res)
|
self.send_index(
|
||||||
|
path,
|
||||||
|
paths,
|
||||||
|
true,
|
||||||
|
query_params,
|
||||||
|
head_only,
|
||||||
|
user,
|
||||||
|
access_paths,
|
||||||
|
res,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_zip_dir(
|
async fn handle_zip_dir(
|
||||||
@@ -593,7 +620,7 @@ impl Server {
|
|||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if path.extension().is_none() {
|
if path.extension().is_none() {
|
||||||
let path = self.args.path.join(INDEX_NAME);
|
let path = self.args.serve_path.join(INDEX_NAME);
|
||||||
self.handle_send_file(&path, headers, head_only, res)
|
self.handle_send_file(&path, headers, head_only, res)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
@@ -609,7 +636,7 @@ impl Server {
|
|||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
|
if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
|
||||||
match self.args.assets_path.as_ref() {
|
match self.args.assets.as_ref() {
|
||||||
Some(assets_path) => {
|
Some(assets_path) => {
|
||||||
let path = assets_path.join(name);
|
let path = assets_path.join(name);
|
||||||
self.handle_send_file(&path, headers, false, res).await?;
|
self.handle_send_file(&path, headers, false, res).await?;
|
||||||
@@ -749,7 +776,10 @@ impl Server {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
|
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
|
||||||
let (file, meta) = (file?, meta?);
|
let (file, meta) = (file?, meta?);
|
||||||
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
|
let href = format!(
|
||||||
|
"/{}",
|
||||||
|
normalize_path(path.strip_prefix(&self.args.serve_path)?)
|
||||||
|
);
|
||||||
let mut buffer: Vec<u8> = vec![];
|
let mut buffer: Vec<u8> = vec![];
|
||||||
file.take(1024).read_to_end(&mut buffer).await?;
|
file.take(1024).read_to_end(&mut buffer).await?;
|
||||||
let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
|
let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
|
||||||
@@ -759,7 +789,7 @@ impl Server {
|
|||||||
uri_prefix: self.args.uri_prefix.clone(),
|
uri_prefix: self.args.uri_prefix.clone(),
|
||||||
allow_upload: self.args.allow_upload,
|
allow_upload: self.args.allow_upload,
|
||||||
allow_delete: self.args.allow_delete,
|
allow_delete: self.args.allow_delete,
|
||||||
auth: self.args.auth.valid(),
|
auth: self.args.auth.exist(),
|
||||||
user,
|
user,
|
||||||
editable,
|
editable,
|
||||||
};
|
};
|
||||||
@@ -767,7 +797,7 @@ impl Server {
|
|||||||
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
||||||
let output = self
|
let output = self
|
||||||
.html
|
.html
|
||||||
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
|
.replace("__ASSETS_PREFIX__", &self.assets_prefix)
|
||||||
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?);
|
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?);
|
||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
.typed_insert(ContentLength(output.as_bytes().len() as u64));
|
.typed_insert(ContentLength(output.as_bytes().len() as u64));
|
||||||
@@ -795,12 +825,15 @@ impl Server {
|
|||||||
},
|
},
|
||||||
None => 1,
|
None => 1,
|
||||||
};
|
};
|
||||||
let mut paths = match self.to_pathitem(path, &self.args.path).await? {
|
let mut paths = match self.to_pathitem(path, &self.args.serve_path).await? {
|
||||||
Some(v) => vec![v],
|
Some(v) => vec![v],
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
if depth != 0 {
|
if depth != 0 {
|
||||||
match self.list_dir(path, &self.args.path, access_paths).await {
|
match self
|
||||||
|
.list_dir(path, &self.args.serve_path, access_paths)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(child) => paths.extend(child),
|
Ok(child) => paths.extend(child),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
status_forbid(res);
|
status_forbid(res);
|
||||||
@@ -820,7 +853,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
|
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
|
||||||
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
|
if let Some(pathitem) = self.to_pathitem(path, &self.args.serve_path).await? {
|
||||||
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
|
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
|
||||||
} else {
|
} else {
|
||||||
status_not_found(res);
|
status_not_found(res);
|
||||||
@@ -920,17 +953,16 @@ impl Server {
|
|||||||
query_params: &HashMap<String, String>,
|
query_params: &HashMap<String, String>,
|
||||||
head_only: bool,
|
head_only: bool,
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
|
access_paths: AccessPaths,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(sort) = query_params.get("sort") {
|
if let Some(sort) = query_params.get("sort") {
|
||||||
if sort == "name" {
|
if sort == "name" {
|
||||||
paths.sort_by(|v1, v2| {
|
paths.sort_by(|v1, v2| v1.sort_by_name(v2))
|
||||||
alphanumeric_sort::compare_str(v1.name.to_lowercase(), v2.name.to_lowercase())
|
|
||||||
})
|
|
||||||
} else if sort == "mtime" {
|
} else if sort == "mtime" {
|
||||||
paths.sort_by(|v1, v2| v1.mtime.cmp(&v2.mtime))
|
paths.sort_by(|v1, v2| v1.sort_by_mtime(v2))
|
||||||
} else if sort == "size" {
|
} else if sort == "size" {
|
||||||
paths.sort_by(|v1, v2| v1.size.unwrap_or(0).cmp(&v2.size.unwrap_or(0)))
|
paths.sort_by(|v1, v2| v1.sort_by_size(v2))
|
||||||
}
|
}
|
||||||
if query_params
|
if query_params
|
||||||
.get("order")
|
.get("order")
|
||||||
@@ -940,7 +972,7 @@ impl Server {
|
|||||||
paths.reverse()
|
paths.reverse()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
paths.sort_unstable();
|
paths.sort_by(|v1, v2| v1.sort_by_name(v2))
|
||||||
}
|
}
|
||||||
if query_params.contains_key("simple") {
|
if query_params.contains_key("simple") {
|
||||||
let output = paths
|
let output = paths
|
||||||
@@ -964,17 +996,21 @@ impl Server {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
|
let href = format!(
|
||||||
|
"/{}",
|
||||||
|
normalize_path(path.strip_prefix(&self.args.serve_path)?)
|
||||||
|
);
|
||||||
|
let readwrite = access_paths.perm().readwrite();
|
||||||
let data = IndexData {
|
let data = IndexData {
|
||||||
kind: DataKind::Index,
|
kind: DataKind::Index,
|
||||||
href,
|
href,
|
||||||
uri_prefix: self.args.uri_prefix.clone(),
|
uri_prefix: self.args.uri_prefix.clone(),
|
||||||
allow_upload: self.args.allow_upload,
|
allow_upload: self.args.allow_upload && readwrite,
|
||||||
allow_delete: self.args.allow_delete,
|
allow_delete: self.args.allow_delete && readwrite,
|
||||||
allow_search: self.args.allow_search,
|
allow_search: self.args.allow_search,
|
||||||
allow_archive: self.args.allow_archive,
|
allow_archive: self.args.allow_archive,
|
||||||
dir_exists: exist,
|
dir_exists: exist,
|
||||||
auth: self.args.auth.valid(),
|
auth: self.args.auth.exist(),
|
||||||
user,
|
user,
|
||||||
paths,
|
paths,
|
||||||
};
|
};
|
||||||
@@ -986,7 +1022,7 @@ impl Server {
|
|||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
||||||
self.html
|
self.html
|
||||||
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
|
.replace("__ASSETS_PREFIX__", &self.assets_prefix)
|
||||||
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?)
|
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?)
|
||||||
};
|
};
|
||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
@@ -999,9 +1035,9 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn auth_reject(&self, res: &mut Response) -> Result<()> {
|
fn auth_reject(&self, res: &mut Response) -> Result<()> {
|
||||||
let value = self.args.auth_method.www_auth(false)?;
|
|
||||||
set_webdav_headers(res);
|
set_webdav_headers(res);
|
||||||
res.headers_mut().insert(WWW_AUTHENTICATE, value.parse()?);
|
res.headers_mut()
|
||||||
|
.append(WWW_AUTHENTICATE, www_authenticate(&self.args)?);
|
||||||
// set 401 to make the browser pop up the login box
|
// set 401 to make the browser pop up the login box
|
||||||
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1011,7 +1047,7 @@ impl Server {
|
|||||||
fs::canonicalize(path)
|
fs::canonicalize(path)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|v| v.starts_with(&self.args.path))
|
.map(|v| v.starts_with(&self.args.serve_path))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,12 +1070,10 @@ impl Server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let authorization = headers.get(AUTHORIZATION);
|
let authorization = headers.get(AUTHORIZATION);
|
||||||
let guard = self.args.auth.guard(
|
let guard = self
|
||||||
&relative_path,
|
.args
|
||||||
req.method(),
|
.auth
|
||||||
authorization,
|
.guard(&relative_path, req.method(), authorization);
|
||||||
self.args.auth_method.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match guard {
|
match guard {
|
||||||
(_, Some(_)) => {}
|
(_, Some(_)) => {}
|
||||||
@@ -1079,14 +1113,14 @@ impl Server {
|
|||||||
|
|
||||||
fn join_path(&self, path: &str) -> Option<PathBuf> {
|
fn join_path(&self, path: &str) -> Option<PathBuf> {
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
return Some(self.args.path.clone());
|
return Some(self.args.serve_path.clone());
|
||||||
}
|
}
|
||||||
let path = if cfg!(windows) {
|
let path = if cfg!(windows) {
|
||||||
path.replace('/', "\\")
|
path.replace('/', "\\")
|
||||||
} else {
|
} else {
|
||||||
path.to_string()
|
path.to_string()
|
||||||
};
|
};
|
||||||
Some(self.args.path.join(path))
|
Some(self.args.serve_path.join(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_dir(
|
async fn list_dir(
|
||||||
@@ -1242,12 +1276,45 @@ impl PathItem {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base_name(&self) -> &str {
|
pub fn base_name(&self) -> &str {
|
||||||
self.name.split('/').last().unwrap_or_default()
|
self.name.split('/').last().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sort_by_name(&self, other: &Self) -> Ordering {
|
||||||
|
match self.path_type.cmp(&other.path_type) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
alphanumeric_sort::compare_str(self.name.to_lowercase(), other.name.to_lowercase())
|
||||||
|
}
|
||||||
|
v => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_by_mtime(&self, other: &Self) -> Ordering {
|
||||||
|
match self.path_type.cmp(&other.path_type) {
|
||||||
|
Ordering::Equal => self.mtime.cmp(&other.mtime),
|
||||||
|
v => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_by_size(&self, other: &Self) -> Ordering {
|
||||||
|
match self.path_type.cmp(&other.path_type) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
if self.is_dir() {
|
||||||
|
alphanumeric_sort::compare_str(
|
||||||
|
self.name.to_lowercase(),
|
||||||
|
other.name.to_lowercase(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.size.unwrap_or(0).cmp(&other.size.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Serialize, Eq, PartialEq)]
|
||||||
enum PathType {
|
enum PathType {
|
||||||
Dir,
|
Dir,
|
||||||
SymlinkDir,
|
SymlinkDir,
|
||||||
@@ -1255,6 +1322,24 @@ enum PathType {
|
|||||||
SymlinkFile,
|
SymlinkFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ord for PathType {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let to_value = |t: &Self| -> u8 {
|
||||||
|
if matches!(t, Self::Dir | Self::SymlinkDir) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
to_value(self).cmp(&to_value(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialOrd for PathType {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_timestamp(time: &SystemTime) -> u64 {
|
fn to_timestamp(time: &SystemTime) -> u64 {
|
||||||
time.duration_since(SystemTime::UNIX_EPOCH)
|
time.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -1286,17 +1371,15 @@ fn add_cors(res: &mut Response) {
|
|||||||
.typed_insert(AccessControlAllowCredentials);
|
.typed_insert(AccessControlAllowCredentials);
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
"Access-Control-Allow-Methods",
|
"Access-Control-Allow-Methods",
|
||||||
HeaderValue::from_static("GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"),
|
HeaderValue::from_static("*"),
|
||||||
);
|
);
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
"Access-Control-Allow-Headers",
|
"Access-Control-Allow-Headers",
|
||||||
HeaderValue::from_static("Authorization,Destination,Range,Content-Type"),
|
HeaderValue::from_static("Authorization,*"),
|
||||||
);
|
);
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
"Access-Control-Expose-Headers",
|
"Access-Control-Expose-Headers",
|
||||||
HeaderValue::from_static(
|
HeaderValue::from_static("Authorization,*"),
|
||||||
"WWW-Authenticate,Content-Range,Accept-Ranges,Content-Disposition",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1329,8 +1412,9 @@ async fn zip_dir<W: AsyncWrite + Unpin>(
|
|||||||
let mut paths: Vec<PathBuf> = vec![];
|
let mut paths: Vec<PathBuf> = vec![];
|
||||||
for dir in access_paths.leaf_paths(&dir_clone) {
|
for dir in access_paths.leaf_paths(&dir_clone) {
|
||||||
let mut it = WalkDir::new(&dir).into_iter();
|
let mut it = WalkDir::new(&dir).into_iter();
|
||||||
|
it.next();
|
||||||
while let Some(Ok(entry)) = it.next() {
|
while let Some(Ok(entry)) = it.next() {
|
||||||
if !running.load(Ordering::SeqCst) {
|
if !running.load(atomic::Ordering::SeqCst) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
|
|||||||
@@ -29,6 +29,49 @@ fn auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HASHED_PASSWORD_AUTH: &str = "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw"; // user:pass
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_hashed_password(
|
||||||
|
#[with(&["--auth", HASHED_PASSWORD_AUTH, "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let url = format!("{}file1", server.url());
|
||||||
|
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
if let Err(err) = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.send_with_digest_auth("user", "pass")
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
format!("{err:?}"),
|
||||||
|
r#"DigestAuth(MissingRequired("realm", "Basic realm=\"DUFS\""))"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.basic_auth("user", Some("pass"))
|
||||||
|
.send()?;
|
||||||
|
assert_eq!(resp.status(), 201);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_and_public(
|
||||||
|
#[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let url = format!("{}file1", server.url());
|
||||||
|
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.send_with_digest_auth("user", "pass")?;
|
||||||
|
assert_eq!(resp.status(), 201);
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
assert_eq!(resp.text()?, "abc");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn auth_skip(#[with(&["--auth", "@/"])] server: TestServer) -> Result<(), Error> {
|
fn auth_skip(#[with(&["--auth", "@/"])] server: TestServer) -> Result<(), Error> {
|
||||||
let resp = reqwest::blocking::get(server.url())?;
|
let resp = reqwest::blocking::get(server.url())?;
|
||||||
@@ -108,8 +151,8 @@ fn auth_nest_share(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(server(&["--auth", "user:pass@/:rw", "--auth-method", "basic", "-A"]), "user", "pass")]
|
#[case(server(&["--auth", "user:pass@/:rw", "-A"]), "user", "pass")]
|
||||||
#[case(server(&["--auth", "u1:p1@/:rw", "--auth-method", "basic", "-A"]), "u1", "p1")]
|
#[case(server(&["--auth", "u1:p1@/:rw", "-A"]), "u1", "p1")]
|
||||||
fn auth_basic(
|
fn auth_basic(
|
||||||
#[case] server: TestServer,
|
#[case] server: TestServer,
|
||||||
#[case] user: &str,
|
#[case] user: &str,
|
||||||
@@ -184,3 +227,34 @@ fn auth_partial_index(
|
|||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn no_auth_propfind_dir(
|
||||||
|
#[with(&["--auth", "user:pass@/:rw", "--auth", "@/dir-assets", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let resp = fetch!(b"PROPFIND", server.url()).send()?;
|
||||||
|
assert_eq!(resp.status(), 207);
|
||||||
|
let body = resp.text()?;
|
||||||
|
assert!(body.contains("<D:href>/dir-assets/</D:href>"));
|
||||||
|
assert!(body.contains("<D:href>/dir1/</D:href>"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_data(
|
||||||
|
#[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let resp = reqwest::blocking::get(server.url())?;
|
||||||
|
let content = resp.text()?;
|
||||||
|
let json = utils::retrive_json(&content).unwrap();
|
||||||
|
assert_eq!(json["allow_delete"], serde_json::Value::Bool(false));
|
||||||
|
assert_eq!(json["allow_upload"], serde_json::Value::Bool(false));
|
||||||
|
let resp = fetch!(b"GET", server.url())
|
||||||
|
.basic_auth("user", Some("pass"))
|
||||||
|
.send()?;
|
||||||
|
let content = resp.text()?;
|
||||||
|
let json = utils::retrive_json(&content).unwrap();
|
||||||
|
assert_eq!(json["allow_delete"], serde_json::Value::Bool(true));
|
||||||
|
assert_eq!(json["allow_upload"], serde_json::Value::Bool(true));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> R
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert!(!urls.is_empty());
|
assert!(!urls.is_empty());
|
||||||
for url in urls {
|
reqwest::blocking::get(urls[0])?.error_for_status()?;
|
||||||
reqwest::blocking::get(url)?.error_for_status()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.kill()?;
|
child.kill()?;
|
||||||
|
|
||||||
|
|||||||
56
tests/config.rs
Normal file
56
tests/config.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
mod fixtures;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_fs::TempDir;
|
||||||
|
use diqwest::blocking::WithDigestAuth;
|
||||||
|
use fixtures::{port, tmpdir, wait_for_port, Error};
|
||||||
|
use rstest::rstest;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> {
|
||||||
|
let config_path = get_config_path().display().to_string();
|
||||||
|
let mut child = Command::cargo_bin("dufs")?
|
||||||
|
.arg(tmpdir.path())
|
||||||
|
.arg("-p")
|
||||||
|
.arg(port.to_string())
|
||||||
|
.args(["--config", &config_path])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
wait_for_port(port);
|
||||||
|
|
||||||
|
let url = format!("http://localhost:{port}/dufs/index.html");
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
|
||||||
|
let url = format!("http://localhost:{port}/dufs/index.html");
|
||||||
|
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
|
||||||
|
assert_eq!(resp.text()?, "This is index.html");
|
||||||
|
|
||||||
|
let url = format!("http://localhost:{port}/dufs?simple");
|
||||||
|
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
|
||||||
|
let text: String = resp.text().unwrap();
|
||||||
|
assert!(text.split('\n').any(|c| c == "dir1/"));
|
||||||
|
assert!(!text.split('\n').any(|c| c == "dir3/"));
|
||||||
|
assert!(!text.split('\n').any(|c| c == "test.txt"));
|
||||||
|
|
||||||
|
let url = format!("http://localhost:{port}/dufs/dir1/upload.txt");
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body("Hello")
|
||||||
|
.send_with_digest_auth("user", "pass")?;
|
||||||
|
assert_eq!(resp.status(), 201);
|
||||||
|
|
||||||
|
child.kill()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> PathBuf {
|
||||||
|
let mut path = std::env::current_dir().expect("Failed to get current directory");
|
||||||
|
path.push("tests");
|
||||||
|
path.push("data");
|
||||||
|
path.push("config.yaml");
|
||||||
|
path
|
||||||
|
}
|
||||||
@@ -19,15 +19,15 @@ fn cors(#[with(&["--enable-cors"])] server: TestServer) -> Result<(), Error> {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-allow-methods").unwrap(),
|
resp.headers().get("access-control-allow-methods").unwrap(),
|
||||||
"GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
|
"*"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-allow-headers").unwrap(),
|
resp.headers().get("access-control-allow-headers").unwrap(),
|
||||||
"Authorization,Destination,Range,Content-Type"
|
"Authorization,*"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-expose-headers").unwrap(),
|
resp.headers().get("access-control-expose-headers").unwrap(),
|
||||||
"WWW-Authenticate,Content-Range,Accept-Ranges,Content-Disposition"
|
"Authorization,*"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
9
tests/data/config.yaml
Normal file
9
tests/data/config.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
bind:
|
||||||
|
- 0.0.0.0
|
||||||
|
path-prefix: dufs
|
||||||
|
hidden:
|
||||||
|
- dir3
|
||||||
|
- test.txt
|
||||||
|
auth:
|
||||||
|
- user:pass@/:rw
|
||||||
|
allow-upload: true
|
||||||
@@ -46,7 +46,7 @@ pub fn tmpdir() -> TempDir {
|
|||||||
let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
|
let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
|
||||||
for file in FILES {
|
for file in FILES {
|
||||||
if *file == BIN_FILE {
|
if *file == BIN_FILE {
|
||||||
tmpdir.child(file).write_binary(b"bin\0\0123").unwrap();
|
tmpdir.child(file).write_binary(b"bin\0\x00123").unwrap();
|
||||||
} else {
|
} else {
|
||||||
tmpdir
|
tmpdir
|
||||||
.child(file)
|
.child(file)
|
||||||
@@ -58,7 +58,7 @@ pub fn tmpdir() -> TempDir {
|
|||||||
if *directory == DIR_ASSETS {
|
if *directory == DIR_ASSETS {
|
||||||
tmpdir
|
tmpdir
|
||||||
.child(format!("{}{}", directory, "index.html"))
|
.child(format!("{}{}", directory, "index.html"))
|
||||||
.write_str("__ASSERTS_PREFIX__index.js;DATA = __INDEX_DATA__")
|
.write_str("__ASSETS_PREFIX__index.js;DATA = __INDEX_DATA__")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
for file in FILES {
|
for file in FILES {
|
||||||
@@ -68,7 +68,7 @@ pub fn tmpdir() -> TempDir {
|
|||||||
if *file == BIN_FILE {
|
if *file == BIN_FILE {
|
||||||
tmpdir
|
tmpdir
|
||||||
.child(format!("{directory}{file}"))
|
.child(format!("{directory}{file}"))
|
||||||
.write_binary(b"bin\0\0123")
|
.write_binary(b"bin\0\x00123")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
tmpdir
|
tmpdir
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ fn get_dir_search3(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn get_dir_search4(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
|
||||||
|
let resp = reqwest::blocking::get(format!("{}dir1?q=dir1&simple", server.url()))?;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let text = resp.text().unwrap();
|
||||||
|
assert!(text.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn head_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
|
fn head_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
|
||||||
let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?;
|
let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::process::{Command, Stdio};
|
|||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], false)]
|
#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], false)]
|
||||||
#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user", "--auth-method", "basic"], true)]
|
#[case(&["-a", "user:pass@/:rw", "--log-format", "$remote_user"], true)]
|
||||||
fn log_remote_user(
|
fn log_remote_user(
|
||||||
tmpdir: TempDir,
|
tmpdir: TempDir,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -41,7 +41,7 @@ fn log_remote_user(
|
|||||||
|
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
let mut buf = [0; 1000];
|
let mut buf = [0; 2048];
|
||||||
let buf_len = stdout.read(&mut buf)?;
|
let buf_len = stdout.read(&mut buf)?;
|
||||||
let output = std::str::from_utf8(&buf[0..buf_len])?;
|
let output = std::str::from_utf8(&buf[0..buf_len])?;
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error
|
|||||||
let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?;
|
let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?;
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
|
|
||||||
let mut buf = [0; 1000];
|
let mut buf = [0; 2048];
|
||||||
let buf_len = stdout.read(&mut buf)?;
|
let buf_len = stdout.read(&mut buf)?;
|
||||||
let output = std::str::from_utf8(&buf[0..buf_len])?;
|
let output = std::str::from_utf8(&buf[0..buf_len])?;
|
||||||
|
|
||||||
22
tests/tls.rs
22
tests/tls.rs
@@ -7,6 +7,8 @@ use predicates::str::contains;
|
|||||||
use reqwest::blocking::ClientBuilder;
|
use reqwest::blocking::ClientBuilder;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::fixtures::port;
|
||||||
|
|
||||||
/// Can start the server with TLS and receive encrypted responses.
|
/// Can start the server with TLS and receive encrypted responses.
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(server(&[
|
#[case(server(&[
|
||||||
@@ -33,8 +35,16 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
|
|||||||
/// Wrong path for cert throws error.
|
/// Wrong path for cert throws error.
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn wrong_path_cert() -> Result<(), Error> {
|
fn wrong_path_cert() -> Result<(), Error> {
|
||||||
|
let port = port().to_string();
|
||||||
Command::cargo_bin("dufs")?
|
Command::cargo_bin("dufs")?
|
||||||
.args(["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"])
|
.args([
|
||||||
|
"--tls-cert",
|
||||||
|
"wrong",
|
||||||
|
"--tls-key",
|
||||||
|
"tests/data/key.pem",
|
||||||
|
"--port",
|
||||||
|
&port,
|
||||||
|
])
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(contains("Failed to access `wrong`"));
|
.stderr(contains("Failed to access `wrong`"));
|
||||||
@@ -45,8 +55,16 @@ fn wrong_path_cert() -> Result<(), Error> {
|
|||||||
/// Wrong paths for key throws errors.
|
/// Wrong paths for key throws errors.
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn wrong_path_key() -> Result<(), Error> {
|
fn wrong_path_key() -> Result<(), Error> {
|
||||||
|
let port = port().to_string();
|
||||||
Command::cargo_bin("dufs")?
|
Command::cargo_bin("dufs")?
|
||||||
.args(["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"])
|
.args([
|
||||||
|
"--tls-cert",
|
||||||
|
"tests/data/cert.pem",
|
||||||
|
"--tls-key",
|
||||||
|
"wrong",
|
||||||
|
"--port",
|
||||||
|
&port,
|
||||||
|
])
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(contains("Failed to access `wrong`"));
|
.stderr(contains("Failed to access `wrong`"));
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ pub fn encode_uri(v: &str) -> String {
|
|||||||
parts.join("/")
|
parts.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retrive_json(content: &str) -> Option<Value> {
|
#[allow(dead_code)]
|
||||||
|
pub fn retrive_json(content: &str) -> Option<Value> {
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
let line = lines.iter().find(|v| v.contains("DATA ="))?;
|
let line = lines.iter().find(|v| v.contains("DATA ="))?;
|
||||||
let line_col = line.find("DATA =").unwrap() + 6;
|
let line_col = line.find("DATA =").unwrap() + 6;
|
||||||
|
|||||||
Reference in New Issue
Block a user