Compare commits

..

51 Commits

Author SHA1 Message Date
sigoden
2c5cc60965 chore: release v0.42.0 (#446) 2024-09-02 11:57:13 +08:00
sigoden
972cf2377f chore: improve bug_report issue template (#444) 2024-08-28 09:40:10 +08:00
sigoden
5b338c40da refactor: make logout works on safari (#442) 2024-08-27 16:07:17 +08:00
sigoden
964bf61c37 refactor: improve logout at asserts/index.js (#440) 2024-08-24 15:52:29 +08:00
sigoden
4bf92cc47a feat: webui support logout (#439) 2024-08-24 15:38:13 +08:00
sigoden
7d17d9c415 fix: login successed but popup Forbidden (#437) 2024-08-22 08:52:50 +08:00
sigoden
1db263efae refactor: some query params work as flag and must not accept a value (#431) 2024-08-14 08:57:46 +08:00
sigoden
5d26103ea2 fix: webui unexpected save-btn when file is non-editable (#429) 2024-08-07 20:38:12 +08:00
Matthias Möller
3727dec115 refactor: date formatting in cache tests (#428) 2024-08-07 20:18:30 +08:00
sigoden
0311c9fb90 chore: update deps 2024-07-24 02:01:02 +00:00
sigoden
e9ce4b2dc3 chore: fix typos 2024-07-24 01:58:55 +00:00
52funny
7aba3fe0b6 fix: garbled characters caused by atob (#422) 2024-07-22 18:10:01 +08:00
sigoden
ca5c3d7c54 feat: base64 index-data to avoid misencoding (#421) 2024-07-22 08:02:32 +08:00
sigoden
ec2b064a9a refactor: remove sabredav-partialupdate from DAV res header (#415) 2024-07-14 08:31:07 +08:00
sigoden
cadea9a3bf chore: update deps 2024-07-11 11:47:05 +00:00
sigoden
3e0e6b2e8a chore: update bug_report issue_template 2024-07-11 11:42:39 +00:00
Matthias Möller
632f7a41bf feat: implements remaining http cache conditionalss (#407)
* implements remaining http conditionals

* computed etag is not optional
2024-06-23 20:25:07 +08:00
sigoden
f1e90686dc refactor: return 400 for propfind request when depth is neither 0 nor 1 (#403) 2024-06-14 22:16:50 +08:00
Need4Swede
dc7a7cbb3f refactor: no inline scripts in HTML (#391)
* Moved 'ready' func call from index.html

Inline script moved to index.js

* Moved <script> out from index.html

* moved inline-styling to css

* minor formatting changes

* changed ratio from const to let

* refactor

* fix tests

---------

Co-authored-by: sigoden <sigoden@gmail.com>
2024-05-31 08:51:59 +08:00
sigoden
ce740b1fb1 chore: release v0.41.0 (#389) 2024-05-22 11:20:24 +08:00
sigoden
1eb69f6806 chore: ui minior refinement 2024-05-22 01:56:06 +00:00
sigoden
5f0369aa39 chore: js format 2024-05-14 09:04:49 +00:00
sigoden
fe2358506d fix: head div overlap main contents when wrap (#386) 2024-05-14 17:04:06 +08:00
sigoden
6b6d69a8ef feat: add log-file option (#383) 2024-05-11 17:13:31 +08:00
sigoden
cb7d417fd3 fix: strange issue that occurs only on Microsoft WebDAV (#382) 2024-05-11 16:18:18 +08:00
sigoden
75f06f749c chore: fix typos and clippy (#379) 2024-05-05 06:23:18 +08:00
sigoden
d0c79a95e5 chore: update issue tempalte for bug report 2024-04-27 04:00:02 +00:00
Qishuai Liu
ffc0991a12 refactor: add fixed-width numerals to date and size on file list page (#378) 2024-04-26 17:34:38 +08:00
sigoden
51f9c87e65 chore: update deps 2024-04-19 01:41:41 +00:00
sigoden
529bb33f0b chore: update ci 2024-04-19 01:39:36 +00:00
sigoden
3d3bb822ee chore: update readme 2024-04-19 01:06:34 +00:00
sigoden
9353b2e759 feat: add api to get the hash of a file (#375) 2024-04-19 08:48:54 +08:00
sigoden
a277698322 chore: update docker 2024-04-07 23:01:59 +00:00
sigoden
0ff2b15c9a refactor: digest_auth related tests (#372) 2024-04-08 06:56:51 +08:00
sigoden
319333cd22 chore: update deps 2024-04-07 21:19:34 +00:00
sigoden
d66c9de8c8 feat: tls handshake timeout (#368) 2024-03-08 10:29:12 +08:00
sigoden
7c0fa3dab7 chore: update deps 2024-03-08 00:52:31 +00:00
sigoden
48066d79e0 chore: fix typo 2024-03-08 00:46:35 +00:00
tobyp
1c41db0c2d fix: timestamp format of getlastmodified in dav xml (#366) 2024-02-22 08:30:01 +08:00
Matthias Möller
76ef7ba0fb chore: removes unnecessary clone (#364) 2024-02-17 20:09:20 +08:00
sigoden
3deac84cc9 chore: add docker pulls badge to readme 2024-02-14 11:54:59 +00:00
sigoden
638b715bc2 chore: release v0.40.0 (#361)
* chore: release v0.40.0

* update deps
2024-02-13 12:05:46 +08:00
sigoden
920b70abc4 refactor: improve resolve_path and handle_assets, abandon guard_path (#360) 2024-02-07 16:27:22 +08:00
sigoden
015713bc6d chore: update deps 2024-02-06 09:32:31 +00:00
sigoden
3c75a9c4cc fix: guard req and destination path (#359) 2024-02-06 17:23:18 +08:00
sigoden
871e8276ff chore: add SECURITY.md 2024-02-05 00:09:25 +00:00
sigoden
f92c8ee91d refactor: improve invalid auth (#356) 2024-01-19 10:25:11 +08:00
sigoden
95eb648411 feat: revert supporting for forbidden permission (#352) 2024-01-17 11:31:26 +08:00
sigoden
3354b1face refactor: do not try to bind ipv6 if no ipv6 (#348) 2024-01-16 09:03:27 +08:00
sigoden
9b348fc945 chore: fix typos 2024-01-15 12:53:59 +00:00
sigoden
e1fabc7349 chore: update readme 2024-01-11 09:07:40 +00:00
31 changed files with 1590 additions and 1027 deletions

View File

@@ -5,14 +5,24 @@ about: Create a report to help us improve
**Problem** **Problem**
<!-- A clear and concise description of what the bug is. --> <!-- Provide a clear and concise description of the bug you're experiencing. What did you expect to happen, and what actually happened? -->
**Configuration**
<!-- Please specify the Dufs command-line arguments or configuration used. -->
<!-- If the issue is related to authentication/permissions, include auth configurations while concealing sensitive information (e.g., passwords). -->
**Log** **Log**
The dufs log is crucial for locating the problem, so please do not omit it. <!-- Attach relevant log outputs that can help diagnose the issue. -->
**Environment:** **Screenshots/Media**
<!-- If applicable, add screenshots or videos that help illustrate the issue, especially for WebUI problems. -->
**Environment Information**
- Dufs version: - Dufs version:
- Browser/Webdav info: - Browser/Webdav info:
- OS info: - OS info:
- Proxy server: e.g. nginx, cloudflare - Proxy server (if any): <!-- e.g. nginx, cloudflare -->

View File

@@ -29,7 +29,7 @@ jobs:
RUSTFLAGS: --deny warnings RUSTFLAGS: --deny warnings
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install Rust Toolchain Components - name: Install Rust Toolchain Components
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable

View File

@@ -54,28 +54,13 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
use-cross: true use-cross: true
cargo-flags: "" cargo-flags: ""
- target: mips-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mipsel-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64el-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
env: env:
BUILD_CMD: cargo BUILD_CMD: cargo
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Check Tag - name: Check Tag
id: check-tag id: check-tag
@@ -94,8 +79,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
# Since rust 1.72, mips platforms are tier 3
toolchain: 1.71
- name: Install cross - name: Install cross
if: matrix.use-cross if: matrix.use-cross
@@ -155,14 +138,12 @@ jobs:
fi fi
- name: Publish Archive - name: Publish Archive
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
if: ${{ startsWith(github.ref, 'refs/tags/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: false draft: false
files: ${{ steps.package.outputs.archive }} files: ${{ steps.package.outputs.archive }}
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }} prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker: docker:
name: Publish to Docker Hub name: Publish to Docker Hub
@@ -171,17 +152,18 @@ jobs:
needs: release needs: release
steps: steps:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
file: Dockerfile-release
build-args: | build-args: |
REPO=${{ github.repository }} REPO=${{ github.repository }}
VER=${{ github.ref_name }} VER=${{ github.ref_name }}
@@ -199,7 +181,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: release needs: release
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable

View File

@@ -2,6 +2,65 @@
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.42.0] - 2024-09-01
### Bug Fixes
- Garbled characters caused by atob ([#422](https://github.com/sigoden/dufs/issues/422))
- Webui unexpected save-btn when file is non-editable ([#429](https://github.com/sigoden/dufs/issues/429))
- Login succeeded but popup `Forbidden` ([#437](https://github.com/sigoden/dufs/issues/437))
### Features
- Implements remaining http cache conditionalss ([#407](https://github.com/sigoden/dufs/issues/407))
- Base64 index-data to avoid misencoding ([#421](https://github.com/sigoden/dufs/issues/421))
- Webui support logout ([#439](https://github.com/sigoden/dufs/issues/439))
### Refactor
- No inline scripts in HTML ([#391](https://github.com/sigoden/dufs/issues/391))
- Return 400 for propfind request when depth is neither 0 nor 1 ([#403](https://github.com/sigoden/dufs/issues/403))
- Remove sabredav-partialupdate from DAV res header ([#415](https://github.com/sigoden/dufs/issues/415))
- Date formatting in cache tests ([#428](https://github.com/sigoden/dufs/issues/428))
- Some query params work as flag and must not accept a value ([#431](https://github.com/sigoden/dufs/issues/431))
- Improve logout at asserts/index.js ([#440](https://github.com/sigoden/dufs/issues/440))
- Make logout works on safari ([#442](https://github.com/sigoden/dufs/issues/442))
## [0.41.0] - 2024-05-22
### Bug Fixes
- Timestamp format of getlastmodified in dav xml ([#366](https://github.com/sigoden/dufs/issues/366))
- Strange issue that occurs only on Microsoft WebDAV ([#382](https://github.com/sigoden/dufs/issues/382))
- Head div overlap main contents when wrap ([#386](https://github.com/sigoden/dufs/issues/386))
### Features
- Tls handshake timeout ([#368](https://github.com/sigoden/dufs/issues/368))
- Add api to get the hash of a file ([#375](https://github.com/sigoden/dufs/issues/375))
- Add log-file option ([#383](https://github.com/sigoden/dufs/issues/383))
### Refactor
- Digest_auth related tests ([#372](https://github.com/sigoden/dufs/issues/372))
- Add fixed-width numerals to date and size on file list page ([#378](https://github.com/sigoden/dufs/issues/378))
## [0.40.0] - 2024-02-13
### Bug Fixes
- Guard req and destination path ([#359](https://github.com/sigoden/dufs/issues/359))
### Features
- Revert supporting for forbidden permission ([#352](https://github.com/sigoden/dufs/issues/352))
### Refactor
- Do not try to bind ipv6 if no ipv6 ([#348](https://github.com/sigoden/dufs/issues/348))
- Improve invalid auth ([#356](https://github.com/sigoden/dufs/issues/356))
- Improve resolve_path and handle_assets, abandon guard_path ([#360](https://github.com/sigoden/dufs/issues/360))
## [0.39.0] - 2024-01-11 ## [0.39.0] - 2024-01-11
### Bug Fixes ### Bug Fixes
@@ -69,7 +128,7 @@ All notable changes to this project will be documented in this file.
- Remove one clone on `assets_prefix` ([#270](https://github.com/sigoden/dufs/issues/270)) - Remove one clone on `assets_prefix` ([#270](https://github.com/sigoden/dufs/issues/270))
- Optimize tests - Optimize tests
- Improve code quanity ([#282](https://github.com/sigoden/dufs/issues/282)) - Improve code quality ([#282](https://github.com/sigoden/dufs/issues/282))
## [0.36.0] - 2023-08-24 ## [0.36.0] - 2023-08-24

1161
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dufs" name = "dufs"
version = "0.39.0" version = "0.42.0"
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"
@@ -11,28 +11,28 @@ categories = ["command-line-utilities", "web-programming::http-server"]
keywords = ["static", "file", "server", "webdav", "cli"] keywords = ["static", "file", "server", "webdav", "cli"]
[dependencies] [dependencies]
clap = { version = "4", features = ["wrap_help", "env"] } clap = { version = "4.5", features = ["wrap_help", "env"] }
clap_complete = "4" clap_complete = "4.5"
chrono = { version = "0.4", default-features = false, features = ["clock"] } 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 = "1.0", features = ["http1", "server"] } hyper = { version = "1", features = ["http1", "server"] }
percent-encoding = "2.3" percent-encoding = "2.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] } futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
async_zip = { version = "0.0.16", default-features = false, features = ["deflate", "bzip2", "xz", "chrono", "tokio"] } async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "bzip2", "xz", "chrono", "tokio"] }
headers = "0.4" headers = "0.4"
mime_guess = "2.0" mime_guess = "2.0"
if-addrs = "0.11" if-addrs = "0.13"
rustls-pemfile = { version = "2.0", optional = true } rustls-pemfile = { version = "2.0", optional = true }
tokio-rustls = { version = "0.25", optional = true } tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring", "tls12"]}
md5 = "0.7" md5 = "0.7"
lazy_static = "1.4" lazy_static = "1.4"
uuid = { version = "1.4", features = ["v4", "fast-rng"] } uuid = { version = "1.7", features = ["v4", "fast-rng"] }
urlencoding = "2.1" urlencoding = "2.1"
xml-rs = "0.8" xml-rs = "0.8"
log = "0.4" log = { version = "0.4", features = ["std"] }
socket2 = "0.5" socket2 = "0.5"
async-stream = "0.3" async-stream = "0.3"
walkdir = "2.3" walkdir = "2.3"
@@ -42,16 +42,17 @@ content_inspector = "0.2"
anyhow = "1.0" anyhow = "1.0"
chardetng = "0.1" chardetng = "0.1"
glob = "0.3" glob = "0.3"
indexmap = "2.0" indexmap = "2.2"
serde_yaml = "0.9" serde_yaml = "0.9"
sha-crypt = "0.5" sha-crypt = "0.5"
base64 = "0.21" base64 = "0.22"
smart-default = "0.7" smart-default = "0.7"
rustls-pki-types = "1.0" rustls-pki-types = "1.2"
hyper-util = { version = "0.1", features = ["server-auto", "tokio"] } hyper-util = { version = "0.1", features = ["server-auto", "tokio"] }
http-body-util = "0.1" http-body-util = "0.1"
bytes = "1.5" bytes = "1.5"
pin-project-lite = "0.2" pin-project-lite = "0.2"
sha2 = "0.10.8"
[features] [features]
default = ["tls"] default = ["tls"]
@@ -59,14 +60,14 @@ tls = ["rustls-pemfile", "tokio-rustls"]
[dev-dependencies] [dev-dependencies]
assert_cmd = "2" assert_cmd = "2"
reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false } reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
assert_fs = "1" assert_fs = "1"
port_check = "0.1" port_check = "0.2"
rstest = "0.18" rstest = "0.22"
regex = "1" regex = "1"
url = "2" url = "2"
diqwest = { version = "2.0", features = ["blocking"], default-features = false }
predicates = "3" predicates = "3"
digest_auth = "0.3.1"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@@ -1,17 +1,12 @@
FROM alpine as builder FROM --platform=linux/amd64 messense/rust-musl-cross:x86_64-musl AS amd64
ARG REPO VER TARGETPLATFORM COPY . .
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ RUN cargo install --path . --root /
TARGET="x86_64-unknown-linux-musl"; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ FROM --platform=linux/amd64 messense/rust-musl-cross:aarch64-musl AS arm64
TARGET="aarch64-unknown-linux-musl"; \ COPY . .
elif [ "$TARGETPLATFORM" = "linux/386" ]; then \ RUN cargo install --path . --root /
TARGET="i686-unknown-linux-musl"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ FROM ${TARGETARCH} AS builder
TARGET="armv7-unknown-linux-musleabihf"; \
fi && \
wget https://github.com/${REPO}/releases/download/${VER}/dufs-${VER}-${TARGET}.tar.gz && \
tar -xf dufs-${VER}-${TARGET}.tar.gz && \
mv dufs /bin/
FROM scratch FROM scratch
COPY --from=builder /bin/dufs /bin/dufs COPY --from=builder /bin/dufs /bin/dufs

19
Dockerfile-release Normal file
View File

@@ -0,0 +1,19 @@
FROM alpine as builder
ARG REPO VER TARGETPLATFORM
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
TARGET="x86_64-unknown-linux-musl"; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
TARGET="aarch64-unknown-linux-musl"; \
elif [ "$TARGETPLATFORM" = "linux/386" ]; then \
TARGET="i686-unknown-linux-musl"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
TARGET="armv7-unknown-linux-musleabihf"; \
fi && \
wget https://github.com/${REPO}/releases/download/${VER}/dufs-${VER}-${TARGET}.tar.gz && \
tar -xf dufs-${VER}-${TARGET}.tar.gz && \
mv dufs /bin/
FROM scratch
COPY --from=builder /bin/dufs /bin/dufs
STOPSIGNAL SIGINT
ENTRYPOINT ["/bin/dufs"]

View File

@@ -2,6 +2,7 @@
[![CI](https://github.com/sigoden/dufs/actions/workflows/ci.yaml/badge.svg)](https://github.com/sigoden/dufs/actions/workflows/ci.yaml) [![CI](https://github.com/sigoden/dufs/actions/workflows/ci.yaml/badge.svg)](https://github.com/sigoden/dufs/actions/workflows/ci.yaml)
[![Crates](https://img.shields.io/crates/v/dufs.svg)](https://crates.io/crates/dufs) [![Crates](https://img.shields.io/crates/v/dufs.svg)](https://crates.io/crates/dufs)
[![Docker Pulls](https://img.shields.io/docker/pulls/sigoden/dufs)](https://hub.docker.com/r/sigoden/dufs)
Dufs is a distinctive utility file server that supports static serving, uploading, searching, accessing control, webdav... Dufs is a distinctive utility file server that supports static serving, uploading, searching, accessing control, webdav...
@@ -30,7 +31,7 @@ cargo install dufs
### With docker ### With docker
``` ```
docker run -v `pwd`:/data -p 5000:5000 --rm -it sigoden/dufs /data -A docker run -v `pwd`:/data -p 5000:5000 --rm sigoden/dufs /data -A
``` ```
### With [Homebrew](https://brew.sh) ### With [Homebrew](https://brew.sh)
@@ -72,6 +73,7 @@ Options:
--render-spa Serve SPA(Single Page Application) --render-spa Serve SPA(Single Page Application)
--assets <path> Set the path to the assets directory for overriding the built-in assets --assets <path> Set the path to the assets directory for overriding the built-in assets
--log-format <format> Customize http log format --log-format <format> Customize http log format
--log-file <file> Specify the file to save logs to, other than stdout/stderr
--compress <level> Set zip compress level [default: low] [possible values: none, low, medium, high] --compress <level> Set zip compress level [default: low] [possible values: none, low, medium, high]
--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-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
@@ -151,63 +153,64 @@ dufs --tls-cert my.crt --tls-key my.key
Upload a file Upload a file
``` ```sh
curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file
``` ```
Download a file Download a file
``` ```sh
curl http://127.0.0.1:5000/path-to-file curl http://127.0.0.1:5000/path-to-file # download the file
curl http://127.0.0.1:5000/path-to-file?hash # retrieve the sha256 hash of the file
``` ```
Download a folder as zip file Download a folder as zip file
``` ```sh
curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip
``` ```
Delete a file/folder Delete a file/folder
``` ```sh
curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder
``` ```
Create a directory Create a directory
``` ```sh
curl -X MKCOL https://127.0.0.1:5000/path-to-folder curl -X MKCOL http://127.0.0.1:5000/path-to-folder
``` ```
Move the file/folder to the new path Move the file/folder to the new path
``` ```sh
curl -X MOVE https://127.0.0.1:5000/path -H "Destination: https://127.0.0.1:5000/new-path" curl -X MOVE http://127.0.0.1:5000/path -H "Destination: http://127.0.0.1:5000/new-path"
``` ```
List/search directory contents List/search directory contents
``` ```sh
curl http://127.0.0.1:5000?q=Dockerfile # search for files, similar to `find -name Dockerfile` curl http://127.0.0.1:5000?q=Dockerfile # search for files, similar to `find -name Dockerfile`
curl http://127.0.0.1:5000?simple # output names only, similar to `ls -1` curl http://127.0.0.1:5000?simple # output names only, similar to `ls -1`
curl http://127.0.0.1:5000?json # output paths in json format curl http://127.0.0.1:5000?json # output paths in json format
``` ```
With authorization With authorization (Both basic or digest auth works)
``` ```sh
curl http://127.0.0.1:5000/file --user user:pass # basic auth curl http://127.0.0.1:5000/file --user user:pass # basic auth
curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth
``` ```
Resumable downloads Resumable downloads
``` ```sh
curl -C- -o file http://127.0.0.1:5000/file curl -C- -o file http://127.0.0.1:5000/file
``` ```
Resumable uploads Resumable uploads
``` ```sh
upload_offset=$(curl -I -s http://127.0.0.1:5000/file | tr -d '\r' | sed -n 's/content-length: //p') upload_offset=$(curl -I -s http://127.0.0.1:5000/file | tr -d '\r' | sed -n 's/content-length: //p')
dd skip=$upload_offset if=file status=none ibs=1 | \ dd skip=$upload_offset if=file status=none ibs=1 | \
curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file
@@ -222,17 +225,17 @@ Dufs supports account based access control. You can control who can do what on w
``` ```
dufs -a admin:admin@/:rw -a guest:guest@/ dufs -a admin:admin@/:rw -a guest:guest@/
dufs -a user:pass@/:rw,/dir1,/dir2:- -a @/ dufs -a user:pass@/:rw,/dir1 -a @/
``` ```
1. Use `@` to separate the account and paths. No account means anonymous user. 1. Use `@` to separate the account and paths. No account means anonymous user.
2. Use `:` to separate the username and password of the account. 2. Use `:` to separate the username and password of the account.
3. Use `,` to separate paths. 3. Use `,` to separate paths.
4. Use path suffix `:rw`, `:ro`, `:-` to set permissions: `read-write`, `read-only`, `forbidden`. `:ro` can be omitted. 4. Use path suffix `:rw`/`:ro` set permissions: `read-write`/`read-only`. `:ro` can be omitted.
- `-a admin:admin@/:rw`: `admin` has complete permissions for all paths. - `-a admin:admin@/:rw`: `admin` has complete permissions for all paths.
- `-a guest:guest@/`: `guest` has read-only permissions for all paths. - `-a guest:guest@/`: `guest` has read-only permissions for all paths.
- `-a user:pass@/:rw,/dir1,/dir2:-`: `user` has read-write permissions for `/*`, has read-only permissions for `/dir1/*`, but is fordden for `/dir2/*`. - `-a user:pass@/:rw,/dir1`: `user` has read-write permissions for `/*`, has read-only permissions for `/dir1/*`.
- `-a @/`: All paths is publicly accessible, everyone can view/download it. - `-a @/`: All paths is publicly accessible, everyone can view/download it.
> There are no restrictions on using ':' and '@' characters in a password. For example, `user:pa:ss@1@/:rw` is valid, the password is `pa:ss@1`. > There are no restrictions on using ':' and '@' characters in a password. For example, `user:pa:ss@1@/:rw` is valid, the password is `pa:ss@1`.
@@ -246,12 +249,12 @@ Create hashed password
``` ```
$ mkpasswd -m sha-512 -s $ mkpasswd -m sha-512 -s
Password: 123456 Password: 123456
$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/ $6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/
``` ```
Use hashed password Use hashed password
``` ```
dufs -a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw' dufs -a 'admin:$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/@/:rw'
``` ```
Two important things for hashed passwords: Two important things for hashed passwords:
@@ -327,7 +330,7 @@ All options can be set using environment variables prefixed with `DUFS_`.
--config <file> DUFS_CONFIG=config.yaml --config <file> 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_PREFIX=/static --path-prefix <path> DUFS_PATH_PREFIX=/dufs
--hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock --hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/" -a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
-A, --allow-all DUFS_ALLOW_ALL=true -A, --allow-all DUFS_ALLOW_ALL=true
@@ -340,9 +343,10 @@ All options can be set using environment variables prefixed with `DUFS_`.
--render-index DUFS_RENDER_INDEX=true --render-index DUFS_RENDER_INDEX=true
--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="" --log-format <format> DUFS_LOG_FORMAT=""
--compress <compress> DUFS_COMPRESS="low" --log-file <file> DUFS_LOG_FILE=./dufs.log
--compress <compress> DUFS_COMPRESS=low
--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
``` ```
@@ -378,6 +382,7 @@ render-try-index: true
render-spa: true render-spa: true
assets: ./assets/ assets: ./assets/
log-format: '$remote_addr "$request" $status $http_user_agent' log-format: '$remote_addr "$request" $status $http_user_agent'
log-file: ./dufs.log
compress: low compress: low
tls-cert: tests/data/cert.pem tls-cert: tests/data/cert.pem
tls-key: tests/data/key_pkcs1.pem tls-key: tests/data/key_pkcs1.pem
@@ -402,7 +407,7 @@ Your assets folder must contains a `index.html` file.
## License ## License
Copyright (c) 2022 dufs-developers. Copyright (c) 2022-2024 dufs-developers.
dufs is made available under the terms of either the MIT License or the Apache License 2.0, at your option. dufs is made available under the terms of either the MIT License or the Apache License 2.0, at your option.

21
SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
The latest release of *dufs* is supported. The fixes for any security issues found will be included
in the next release.
## Reporting a Vulnerability
Please [use *dufs*'s security advisory reporting tool provided by
GitHub](https://github.com/sigoden/dufs/security/advisories/new) to report security issues.
We strive to fix security issues as quickly as possible. Across the industry, often the developers'
slowness in developing and releasing a fix is the biggest delay in the process; we take pride in
minimizing this delay as much as we practically can. We encourage you to also minimize the delay
between when you find an issue and when you contact us. You do not need to convince us to take your
report seriously. You don't need to create a PoC or a patch if that would slow down your reporting.
You don't need an elaborate write-up. A short, informal note about the issue is good. We can always
communicate later to fill in any details we need after that first note is shared with us.

0
assets/favicon.ico Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -6,7 +6,7 @@ html {
body { body {
/* prevent premature breadcrumb wrapping on mobile */ /* prevent premature breadcrumb wrapping on mobile */
min-width: 500px; min-width: 538px;
margin: 0; margin: 0;
} }
@@ -19,14 +19,15 @@ body {
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
padding: 0.6em 1em; padding: 0.6em 1em;
position: fixed; position: sticky;
width: 100%; top: 0;
background-color: white; background-color: white;
} }
.breadcrumb { .breadcrumb {
font-size: 1.25em; font-size: 1.25em;
padding-right: 0.6em; padding-right: 0.6em;
word-break: break-all;
} }
.breadcrumb>a { .breadcrumb>a {
@@ -108,7 +109,7 @@ body {
} }
.main { .main {
padding: 3.3em 1em 0; padding: 0 1em;
} }
.empty-folder { .empty-folder {
@@ -153,18 +154,20 @@ body {
.paths-table .cell-actions { .paths-table .cell-actions {
width: 90px; width: 90px;
display: flex; display: flex;
padding-left: 0.6em; padding-left: 0.5em;
} }
.paths-table .cell-mtime { .paths-table .cell-mtime {
width: 120px; width: 120px;
padding-left: 0.6em; padding-left: 0.5em;
font-variant-numeric: tabular-nums;
} }
.paths-table .cell-size { .paths-table .cell-size {
text-align: right; text-align: right;
width: 70px; width: 70px;
padding-left: 0.6em; padding-left: 0.5em;
font-variant-numeric: tabular-nums;
} }
.path svg { .path svg {
@@ -186,7 +189,7 @@ body {
display: block; display: block;
text-decoration: none; text-decoration: none;
max-width: calc(100vw - 375px); max-width: calc(100vw - 375px);
min-width: 200px; min-width: 170px;
} }
.path a:hover { .path a:hover {
@@ -219,15 +222,20 @@ body {
margin-right: 2em; margin-right: 2em;
} }
.login-btn {
cursor: pointer;
}
.save-btn { .save-btn {
cursor: pointer; cursor: pointer;
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
.user-btn { .logout-btn {
display: flex; cursor: pointer;
align-items: center; display: flex;
align-items: center;
} }
.user-name { .user-name {

View File

@@ -6,10 +6,6 @@
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="__ASSETS_PREFIX__favicon.ico"> <link rel="icon" type="image/x-icon" href="__ASSETS_PREFIX__favicon.ico">
<link rel="stylesheet" href="__ASSETS_PREFIX__index.css"> <link rel="stylesheet" href="__ASSETS_PREFIX__index.css">
<script>
DATA = __INDEX_DATA__
</script>
<script src="__ASSETS_PREFIX__index.js"></script>
</head> </head>
<body> <body>
@@ -74,11 +70,12 @@
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" /> d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" />
</svg> </svg>
</div> </div>
<input id="search" title="Searching for folders or files" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1"> <input id="search" title="Searching for folders or files" name="q" type="text" maxlength="128" autocomplete="off"
tabindex="1">
<input type="submit" hidden /> <input type="submit" hidden />
</form> </form>
<div class="toolbox-right"> <div class="toolbox-right">
<div class="login-btn hidden" title="Login for upload/move/delete/edit permissions"> <div class="login-btn hidden" title="Login">
<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"
d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z" /> d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z" />
@@ -86,10 +83,10 @@
d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" /> d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
</svg> </svg>
</div> </div>
<div class="user-btn hidden"> <div class="logout-btn hidden" title="Logout">
<svg width="16" height="16" viewBox="0 0 16 16"> <svg width="16" height="16" viewBox="0 0 16 16">
<path <path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0z"/>
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" /> <path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708z"/>
</svg> </svg>
<span class="user-name"></span> <span class="user-name"></span>
</div> </div>
@@ -125,9 +122,8 @@
<textarea id="editor" class="editor hidden" aria-label="Editor" cols="10"></textarea> <textarea id="editor" class="editor hidden" aria-label="Editor" cols="10"></textarea>
</div> </div>
</div> </div>
<script> <template id="index-data">__INDEX_DATA__</template>
window.addEventListener("DOMContentLoaded", ready); <script src="__ASSETS_PREFIX__index.js"></script>
</script>
</body> </body>
</html> </html>

View File

@@ -29,6 +29,11 @@ var DUFS_MAX_UPLOADINGS = 1;
*/ */
var DATA; var DATA;
/**
* @type {string}
*/
var DIR_EMPTY_NOTE;
/** /**
* @type {PARAMS} * @type {PARAMS}
* @typedef {object} PARAMS * @typedef {object} PARAMS
@@ -45,8 +50,6 @@ const IFRAME_FORMATS = [
".mp3", ".ogg", ".wav", ".m4a", ".mp3", ".ogg", ".wav", ".m4a",
]; ];
const dirEmptyNote = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
const ICONS = { const ICONS = {
dir: `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"></path></svg>`, dir: `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"></path></svg>`,
symlinkFile: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM6 4.5l4 3-4 3v-2c-.98-.02-1.84.22-2.55.7-.71.48-1.19 1.25-1.45 2.3.02-1.64.39-2.88 1.13-3.73.73-.84 1.69-1.27 2.88-1.27v-2H6z"></path></svg>`, symlinkFile: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM6 4.5l4 3-4 3v-2c-.98-.02-1.84.22-2.55.7-.71.48-1.19 1.25-1.45 2.3.02-1.64.39-2.88 1.13-3.73.73-.84 1.69-1.27 2.88-1.27v-2H6z"></path></svg>`,
@@ -91,44 +94,61 @@ let $editor;
/** /**
* @type Element * @type Element
*/ */
let $userBtn; let $loginBtn;
/**
* @type Element
*/
let $logoutBtn;
/** /**
* @type Element * @type Element
*/ */
let $userName; let $userName;
function ready() { // Produce table when window loads
$pathsTable = document.querySelector(".paths-table") window.addEventListener("DOMContentLoaded", async () => {
const $indexData = document.getElementById('index-data');
if (!$indexData) {
alert("No data");
return;
}
DATA = JSON.parse(decodeBase64($indexData.innerHTML));
DIR_EMPTY_NOTE = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
await ready();
});
async function ready() {
$pathsTable = document.querySelector(".paths-table");
$pathsTableHead = document.querySelector(".paths-table thead"); $pathsTableHead = document.querySelector(".paths-table thead");
$pathsTableBody = document.querySelector(".paths-table tbody"); $pathsTableBody = document.querySelector(".paths-table tbody");
$uploadersTable = document.querySelector(".uploaders-table"); $uploadersTable = document.querySelector(".uploaders-table");
$emptyFolder = document.querySelector(".empty-folder"); $emptyFolder = document.querySelector(".empty-folder");
$editor = document.querySelector(".editor"); $editor = document.querySelector(".editor");
$userBtn = document.querySelector(".user-btn"); $loginBtn = document.querySelector(".login-btn");
$logoutBtn = document.querySelector(".logout-btn");
$userName = document.querySelector(".user-name"); $userName = document.querySelector(".user-name");
addBreadcrumb(DATA.href, DATA.uri_prefix); addBreadcrumb(DATA.href, DATA.uri_prefix);
if (DATA.kind == "Index") { if (DATA.kind === "Index") {
document.title = `Index of ${DATA.href} - Dufs`; document.title = `Index of ${DATA.href} - Dufs`;
document.querySelector(".index-page").classList.remove("hidden"); document.querySelector(".index-page").classList.remove("hidden");
setupIndexPage(); await setupIndexPage();
} else if (DATA.kind === "Edit") {
} else if (DATA.kind == "Edit") {
document.title = `Edit ${DATA.href} - Dufs`; document.title = `Edit ${DATA.href} - Dufs`;
document.querySelector(".editor-page").classList.remove("hidden");; document.querySelector(".editor-page").classList.remove("hidden");
setupEditorPage(); await setupEditorPage();
} else if (DATA.kind == "View") { } else if (DATA.kind === "View") {
document.title = `View ${DATA.href} - Dufs`; document.title = `View ${DATA.href} - Dufs`;
document.querySelector(".editor-page").classList.remove("hidden");; document.querySelector(".editor-page").classList.remove("hidden");
setupEditorPage(); await setupEditorPage();
} }
} }
class Uploader { class Uploader {
/** /**
* *
@@ -222,7 +242,7 @@ class Uploader {
uploadOffset = parseInt(value) || 0; uploadOffset = parseInt(value) || 0;
} }
this.uploadOffset = uploadOffset; this.uploadOffset = uploadOffset;
this.ajax() this.ajax();
} }
progress(event) { progress(event) {
@@ -231,7 +251,7 @@ class Uploader {
const [speedValue, speedUnit] = formatSize(speed); const [speedValue, speedUnit] = formatSize(speed);
const speedText = `${speedValue} ${speedUnit}/s`; const speedText = `${speedValue} ${speedUnit}/s`;
const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100); const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100);
const duration = formatDuration((event.total - event.loaded) / speed) const duration = formatDuration((event.total - event.loaded) / speed);
this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`; this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
this.uploaded = event.loaded; this.uploaded = event.loaded;
this.lastUptime = now; this.lastUptime = now;
@@ -275,7 +295,7 @@ Uploader.runQueue = async () => {
if (!Uploader.auth) { if (!Uploader.auth) {
Uploader.auth = true; Uploader.auth = true;
try { try {
await checkAuth() await checkAuth();
} catch { } catch {
Uploader.auth = false; Uploader.auth = false;
} }
@@ -320,7 +340,7 @@ function addBreadcrumb(href, uri_prefix) {
} }
} }
function setupIndexPage() { async function setupIndexPage() {
if (DATA.allow_archive) { if (DATA.allow_archive) {
const $download = document.querySelector(".download"); const $download = document.querySelector(".download");
$download.href = baseUrl() + "?zip"; $download.href = baseUrl() + "?zip";
@@ -336,11 +356,11 @@ function setupIndexPage() {
} }
if (DATA.auth) { if (DATA.auth) {
setupAuth(); await setupAuth();
} }
if (DATA.allow_search) { if (DATA.allow_search) {
setupSearch() setupSearch();
} }
renderPathsTableHead(); renderPathsTableHead();
@@ -403,7 +423,7 @@ function renderPathsTableBody() {
addPath(DATA.paths[i], i); addPath(DATA.paths[i], i);
} }
} else { } else {
$emptyFolder.textContent = dirEmptyNote; $emptyFolder.textContent = DIR_EMPTY_NOTE;
$emptyFolder.classList.remove("hidden"); $emptyFolder.classList.remove("hidden");
} }
} }
@@ -415,7 +435,7 @@ function renderPathsTableBody() {
*/ */
function addPath(file, index) { function addPath(file, index) {
const encodedName = encodedStr(file.name); const encodedName = encodedStr(file.name);
let url = newUrl(file.name) let url = newUrl(file.name);
let actionDelete = ""; let actionDelete = "";
let actionDownload = ""; let actionDownload = "";
let actionMove = ""; let actionMove = "";
@@ -456,7 +476,7 @@ function addPath(file, index) {
${actionMove} ${actionMove}
${actionDelete} ${actionDelete}
${actionEdit} ${actionEdit}
</td>` </td>`;
$pathsTableBody.insertAdjacentHTML("beforeend", ` $pathsTableBody.insertAdjacentHTML("beforeend", `
<tr id="addPath${index}"> <tr id="addPath${index}">
@@ -469,7 +489,7 @@ function addPath(file, index) {
<td class="cell-mtime">${formatMtime(file.mtime)}</td> <td class="cell-mtime">${formatMtime(file.mtime)}</td>
<td class="cell-size">${formatSize(file.size).join(" ")}</td> <td class="cell-size">${formatSize(file.size).join(" ")}</td>
${actionCell} ${actionCell}
</tr>`) </tr>`);
} }
function setupDropzone() { function setupDropzone() {
@@ -481,7 +501,7 @@ function setupDropzone() {
}); });
document.addEventListener("drop", async e => { document.addEventListener("drop", async e => {
if (!e.dataTransfer.items[0].webkitGetAsEntry) { if (!e.dataTransfer.items[0].webkitGetAsEntry) {
const files = e.dataTransfer.files.filter(v => v.size > 0); const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
for (const file of files) { for (const file of files) {
new Uploader(file, []).upload(); new Uploader(file, []).upload();
} }
@@ -491,25 +511,23 @@ function setupDropzone() {
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
entries.push(e.dataTransfer.items[i].webkitGetAsEntry()); entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
} }
addFileEntries(entries, []) addFileEntries(entries, []);
} }
}); });
} }
function setupAuth() { async function setupAuth() {
if (DATA.user) { if (DATA.user) {
$userBtn.classList.remove("hidden"); $logoutBtn.classList.remove("hidden");
$logoutBtn.addEventListener("click", logout);
$userName.textContent = DATA.user; $userName.textContent = DATA.user;
} else { } else {
const $loginBtn = document.querySelector(".login-btn");
$loginBtn.classList.remove("hidden"); $loginBtn.classList.remove("hidden");
$loginBtn.addEventListener("click", async () => { $loginBtn.addEventListener("click", async () => {
try { try {
await checkAuth() await checkAuth();
location.reload(); } catch {}
} catch (err) { location.reload();
alert(err.message);
}
}); });
} }
} }
@@ -586,11 +604,13 @@ async function setupEditorPage() {
await doDeletePath(name, url, () => { await doDeletePath(name, url, () => {
location.href = location.href.split("/").slice(0, -1).join("/"); location.href = location.href.split("/").slice(0, -1).join("/");
}); });
}) });
const $saveBtn = document.querySelector(".save-btn"); if (DATA.editable) {
$saveBtn.classList.remove("hidden"); const $saveBtn = document.querySelector(".save-btn");
$saveBtn.addEventListener("click", saveChange); $saveBtn.classList.remove("hidden");
$saveBtn.addEventListener("click", saveChange);
}
} else if (DATA.kind == "View") { } else if (DATA.kind == "View") {
$editor.readonly = true; $editor.readonly = true;
} }
@@ -600,7 +620,7 @@ async function setupEditorPage() {
const url = baseUrl(); const url = baseUrl();
const ext = extName(baseName(url)); const ext = extName(baseName(url));
if (IFRAME_FORMATS.find(v => v === ext)) { if (IFRAME_FORMATS.find(v => v === ext)) {
$notEditable.insertAdjacentHTML("afterend", `<iframe src="${url}" sandbox width="100%" height="${window.innerHeight - 100}px"></iframe>`) $notEditable.insertAdjacentHTML("afterend", `<iframe src="${url}" sandbox width="100%" height="${window.innerHeight - 100}px"></iframe>`);
} else { } else {
$notEditable.classList.remove("hidden"); $notEditable.classList.remove("hidden");
$notEditable.textContent = "Cannot edit because file is too large or binary."; $notEditable.textContent = "Cannot edit because file is too large or binary.";
@@ -617,8 +637,8 @@ async function setupEditorPage() {
$editor.value = await res.text(); $editor.value = await res.text();
} else { } else {
const bytes = await res.arrayBuffer(); const bytes = await res.arrayBuffer();
const dataView = new DataView(bytes) const dataView = new DataView(bytes);
const decoder = new TextDecoder(encoding) const decoder = new TextDecoder(encoding);
$editor.value = decoder.decode(dataView); $editor.value = decoder.decode(dataView);
} }
} catch (err) { } catch (err) {
@@ -639,10 +659,10 @@ async function deletePath(index) {
DATA.paths[index] = null; DATA.paths[index] = null;
if (!DATA.paths.find(v => !!v)) { if (!DATA.paths.find(v => !!v)) {
$pathsTable.classList.add("hidden"); $pathsTable.classList.add("hidden");
$emptyFolder.textContent = dirEmptyNote; $emptyFolder.textContent = DIR_EMPTY_NOTE;
$emptyFolder.classList.remove("hidden"); $emptyFolder.classList.remove("hidden");
} }
}) });
} }
async function doDeletePath(name, url, cb) { async function doDeletePath(name, url, cb) {
@@ -675,13 +695,13 @@ async function movePath(index) {
} }
async function doMovePath(fileUrl) { async function doMovePath(fileUrl) {
const fileUrlObj = new URL(fileUrl) const fileUrlObj = new URL(fileUrl);
const prefix = DATA.uri_prefix.slice(0, -1); const prefix = DATA.uri_prefix.slice(0, -1);
const filePath = decodeURIComponent(fileUrlObj.pathname.slice(prefix.length)); const filePath = decodeURIComponent(fileUrlObj.pathname.slice(prefix.length));
let newPath = prompt("Enter new path", filePath) let newPath = prompt("Enter new path", filePath);
if (!newPath) return; if (!newPath) return;
if (!newPath.startsWith("/")) newPath = "/" + newPath; if (!newPath.startsWith("/")) newPath = "/" + newPath;
if (filePath === newPath) return; if (filePath === newPath) return;
@@ -729,12 +749,23 @@ async function saveChange() {
async function checkAuth() { async function checkAuth() {
if (!DATA.auth) return; if (!DATA.auth) return;
const res = await fetch(baseUrl(), { const res = await fetch(baseUrl(), {
method: "WRITEABLE", method: "CHECKAUTH",
}); });
await assertResOK(res); await assertResOK(res);
document.querySelector(".login-btn").classList.add("hidden"); $loginBtn.classList.add("hidden");
$userBtn.classList.remove("hidden"); $logoutBtn.classList.remove("hidden");
$userName.textContent = ""; $userName.textContent = await res.text();
}
function logout() {
if (!DATA.auth) return;
const url = baseUrl();
const xhr = new XMLHttpRequest();
xhr.open("LOGOUT", url, true, DATA.user);
xhr.onload = () => {
location.href = url;
}
xhr.send();
} }
/** /**
@@ -804,7 +835,7 @@ function baseUrl() {
} }
function baseName(url) { function baseName(url) {
return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]) return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]);
} }
function extName(filename) { function extName(filename) {
@@ -831,7 +862,7 @@ function getPathSvg(path_type) {
} }
function formatMtime(mtime) { function formatMtime(mtime) {
if (!mtime) return "" if (!mtime) return "";
const date = new Date(mtime); const date = new Date(mtime);
const year = date.getFullYear(); const year = date.getFullYear();
const month = padZero(date.getMonth() + 1, 2); const month = padZero(date.getMonth() + 1, 2);
@@ -842,17 +873,17 @@ function formatMtime(mtime) {
} }
function padZero(value, size) { function padZero(value, size) {
return ("0".repeat(size) + value).slice(-1 * size) return ("0".repeat(size) + value).slice(-1 * size);
} }
function formatSize(size) { function formatSize(size) {
if (size == null) return [0, "B"] if (size == null) return [0, "B"];
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (size == 0) return [0, "B"]; if (size == 0) return [0, "B"];
const i = parseInt(Math.floor(Math.log(size) / Math.log(1024))); const i = parseInt(Math.floor(Math.log(size) / Math.log(1024)));
ratio = 1 let ratio = 1;
if (i >= 3) { if (i >= 3) {
ratio = 100 ratio = 100;
} }
return [Math.round(size * ratio / Math.pow(1024, i), 2) / ratio, sizes[i]]; return [Math.round(size * ratio / Math.pow(1024, i), 2) / ratio, sizes[i]];
} }
@@ -861,7 +892,7 @@ function formatDuration(seconds) {
seconds = Math.ceil(seconds); seconds = Math.ceil(seconds);
const h = Math.floor(seconds / 3600); const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds - h * 3600) / 60); const m = Math.floor((seconds - h * 3600) / 60);
const s = seconds - h * 3600 - m * 60 const s = seconds - h * 3600 - m * 60;
return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`; return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`;
} }
@@ -886,12 +917,31 @@ async function assertResOK(res) {
} }
function getEncoding(contentType) { function getEncoding(contentType) {
const charset = contentType?.split(";")[1]; const charset = contentType?.split(";")[1];
if (/charset/i.test(charset)) { if (/charset/i.test(charset)) {
let encoding = charset.split("=")[1]; let encoding = charset.split("=")[1];
if (encoding) { if (encoding) {
return encoding.toLowerCase() return encoding.toLowerCase();
}
} }
return 'utf-8' }
return 'utf-8';
}
// Parsing base64 strings with Unicode characters
function decodeBase64(base64String) {
const binString = atob(base64String);
const len = binString.length;
const bytes = new Uint8Array(len);
const arr = new Uint32Array(bytes.buffer, 0, Math.floor(len / 4));
let i = 0;
for (; i < arr.length; i++) {
arr[i] = binString.charCodeAt(i * 4) |
(binString.charCodeAt(i * 4 + 1) << 8) |
(binString.charCodeAt(i * 4 + 2) << 16) |
(binString.charCodeAt(i * 4 + 3) << 24);
}
for (i = i * 4; i < len; i++) {
bytes[i] = binString.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
} }

View File

@@ -197,6 +197,15 @@ pub fn build_cli() -> Command {
.value_name("format") .value_name("format")
.help("Customize http log format"), .help("Customize http log format"),
) )
.arg(
Arg::new("log-file")
.env("DUFS_LOG_FILE")
.hide_env(true)
.long("log-file")
.value_name("file")
.value_parser(value_parser!(PathBuf))
.help("Specify the file to save logs to, other than stdout/stderr"),
)
.arg( .arg(
Arg::new("compress") Arg::new("compress")
.env("DUFS_COMPRESS") .env("DUFS_COMPRESS")
@@ -280,6 +289,7 @@ pub struct Args {
#[serde(deserialize_with = "deserialize_log_http")] #[serde(deserialize_with = "deserialize_log_http")]
#[serde(rename = "log-format")] #[serde(rename = "log-format")]
pub http_logger: HttpLogger, pub http_logger: HttpLogger,
pub log_file: Option<PathBuf>,
pub compress: Compress, pub compress: Compress,
pub tls_cert: Option<PathBuf>, pub tls_cert: Option<PathBuf>,
pub tls_key: Option<PathBuf>, pub tls_key: Option<PathBuf>,
@@ -301,7 +311,7 @@ impl Args {
} }
if let Some(path) = matches.get_one::<PathBuf>("serve-path") { if let Some(path) = matches.get_one::<PathBuf>("serve-path") {
args.serve_path = path.clone() args.serve_path.clone_from(path)
} }
args.serve_path = Self::sanitize_path(args.serve_path)?; args.serve_path = Self::sanitize_path(args.serve_path)?;
@@ -317,7 +327,7 @@ impl Args {
args.path_is_file = args.serve_path.metadata()?.is_file(); args.path_is_file = args.serve_path.metadata()?.is_file();
if let Some(path_prefix) = matches.get_one::<String>("path-prefix") { if let Some(path_prefix) = matches.get_one::<String>("path-prefix") {
args.path_prefix = path_prefix.clone(); args.path_prefix.clone_from(path_prefix)
} }
args.path_prefix = args.path_prefix.trim_matches('/').to_string(); args.path_prefix = args.path_prefix.trim_matches('/').to_string();
@@ -392,6 +402,10 @@ impl Args {
args.http_logger = log_format.parse()?; args.http_logger = log_format.parse()?;
} }
if let Some(log_file) = matches.get_one::<PathBuf>("log-file") {
args.log_file = Some(log_file.clone());
}
if let Some(compress) = matches.get_one::<Compress>("compress") { if let Some(compress) = matches.get_one::<Compress>("compress") {
args.compress = *compress; args.compress = *compress;
} }

View File

@@ -1,7 +1,7 @@
use crate::{args::Args, server::Response, utils::unix_now}; use crate::{args::Args, server::Response, utils::unix_now};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose::STANDARD, Engine as _};
use headers::HeaderValue; use headers::HeaderValue;
use hyper::{header::WWW_AUTHENTICATE, Method}; use hyper::{header::WWW_AUTHENTICATE, Method};
use indexmap::IndexMap; use indexmap::IndexMap;
@@ -29,15 +29,15 @@ lazy_static! {
pub struct AccessControl { pub struct AccessControl {
use_hashed_password: bool, use_hashed_password: bool,
users: IndexMap<String, (String, AccessPaths)>, users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>, anonymous: Option<AccessPaths>,
} }
impl Default for AccessControl { impl Default for AccessControl {
fn default() -> Self { fn default() -> Self {
AccessControl { AccessControl {
use_hashed_password: false, use_hashed_password: false,
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(), users: IndexMap::new(),
anonymous: Some(AccessPaths::new(AccessPerm::ReadWrite)),
} }
} }
} }
@@ -66,15 +66,15 @@ impl AccessControl {
account_paths_pairs.push((user, pass, paths)); account_paths_pairs.push((user, pass, paths));
} }
} }
let mut anony = None; let mut anonymous = None;
if let Some(paths) = annoy_paths { if let Some(paths) = annoy_paths {
let mut access_paths = AccessPaths::default(); let mut access_paths = AccessPaths::default();
access_paths.merge(paths); access_paths.merge(paths);
anony = Some(access_paths); anonymous = Some(access_paths);
} }
let mut users = IndexMap::new(); let mut users = IndexMap::new();
for (user, pass, paths) in account_paths_pairs.into_iter() { for (user, pass, paths) in account_paths_pairs.into_iter() {
let mut access_paths = anony.clone().unwrap_or_default(); let mut access_paths = anonymous.clone().unwrap_or_default();
access_paths access_paths
.merge(paths) .merge(paths)
.ok_or_else(|| anyhow!("Invalid auth `{user}:{pass}@{paths}"))?; .ok_or_else(|| anyhow!("Invalid auth `{user}:{pass}@{paths}"))?;
@@ -87,7 +87,7 @@ impl AccessControl {
Ok(Self { Ok(Self {
use_hashed_password, use_hashed_password,
users, users,
anony, anonymous,
}) })
} }
@@ -100,6 +100,7 @@ impl AccessControl {
path: &str, path: &str,
method: &Method, method: &Method,
authorization: Option<&HeaderValue>, authorization: Option<&HeaderValue>,
guard_options: bool,
) -> (Option<String>, Option<AccessPaths>) { ) -> (Option<String>, Option<AccessPaths>) {
if let Some(authorization) = authorization { if let Some(authorization) = authorization {
if let Some(user) = get_auth_user(authorization) { if let Some(user) = get_auth_user(authorization) {
@@ -109,18 +110,18 @@ impl AccessControl {
} }
if check_auth(authorization, method.as_str(), &user, pass).is_some() { if check_auth(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 {
return (None, None);
} }
} }
} }
return (None, None);
} }
if method == Method::OPTIONS { if !guard_options && method == Method::OPTIONS {
return (None, Some(AccessPaths::new(AccessPerm::ReadOnly))); return (None, Some(AccessPaths::new(AccessPerm::ReadOnly)));
} }
if let Some(paths) = self.anony.as_ref() { if let Some(paths) = self.anonymous.as_ref() {
return (None, paths.find(path, !is_readonly_method(method))); return (None, paths.find(path, !is_readonly_method(method)));
} }
@@ -147,7 +148,7 @@ impl AccessPaths {
} }
pub fn set_perm(&mut self, perm: AccessPerm) { pub fn set_perm(&mut self, perm: AccessPerm) {
if !perm.inherit() { if !perm.indexonly() {
self.perm = perm; self.perm = perm;
} }
} }
@@ -158,7 +159,6 @@ impl AccessPaths {
None => (item, AccessPerm::ReadOnly), None => (item, AccessPerm::ReadOnly),
Some((path, "ro")) => (path, AccessPerm::ReadOnly), Some((path, "ro")) => (path, AccessPerm::ReadOnly),
Some((path, "rw")) => (path, AccessPerm::ReadWrite), Some((path, "rw")) => (path, AccessPerm::ReadWrite),
Some((path, "-")) => (path, AccessPerm::Forbidden),
_ => return None, _ => return None,
}; };
self.add(path, perm); self.add(path, perm);
@@ -193,9 +193,6 @@ impl AccessPaths {
.filter(|v| !v.is_empty()) .filter(|v| !v.is_empty())
.collect(); .collect();
let target = self.find_impl(&parts, self.perm)?; let target = self.find_impl(&parts, self.perm)?;
if target.perm().forbidden() {
return None;
}
if writable && !target.perm().readwrite() { if writable && !target.perm().readwrite() {
return None; return None;
} }
@@ -203,13 +200,13 @@ impl AccessPaths {
} }
fn find_impl(&self, parts: &[&str], perm: AccessPerm) -> Option<AccessPaths> { fn find_impl(&self, parts: &[&str], perm: AccessPerm) -> Option<AccessPaths> {
let perm = if !self.perm.inherit() { let perm = if !self.perm.indexonly() {
self.perm self.perm
} else { } else {
perm perm
}; };
if parts.is_empty() { if parts.is_empty() {
if perm.inherit() { if perm.indexonly() {
return Some(self.clone()); return Some(self.clone());
} else { } else {
return Some(AccessPaths::new(perm)); return Some(AccessPaths::new(perm));
@@ -218,7 +215,7 @@ impl AccessPaths {
let child = match self.children.get(parts[0]) { let child = match self.children.get(parts[0]) {
Some(v) => v, Some(v) => v,
None => { None => {
if perm.inherit() { if perm.indexonly() {
return None; return None;
} else { } else {
return Some(AccessPaths::new(perm)); return Some(AccessPaths::new(perm));
@@ -233,7 +230,7 @@ impl AccessPaths {
} }
pub fn child_paths(&self, base: &Path) -> Vec<PathBuf> { pub fn child_paths(&self, base: &Path) -> Vec<PathBuf> {
if !self.perm().inherit() { if !self.perm().indexonly() {
return vec![base.to_path_buf()]; return vec![base.to_path_buf()];
} }
let mut output = vec![]; let mut output = vec![];
@@ -244,7 +241,7 @@ impl AccessPaths {
fn child_paths_impl(&self, output: &mut Vec<PathBuf>, base: &Path) { fn child_paths_impl(&self, output: &mut Vec<PathBuf>, base: &Path) {
for (name, child) in self.children.iter() { for (name, child) in self.children.iter() {
let base = base.join(name); let base = base.join(name);
if child.perm().inherit() { if child.perm().indexonly() {
child.child_paths_impl(output, &base); child.child_paths_impl(output, &base);
} else { } else {
output.push(base) output.push(base)
@@ -256,24 +253,19 @@ impl AccessPaths {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum AccessPerm { pub enum AccessPerm {
#[default] #[default]
Inherit, IndexOnly,
ReadOnly, ReadOnly,
ReadWrite, ReadWrite,
Forbidden,
} }
impl AccessPerm { impl AccessPerm {
pub fn inherit(&self) -> bool { pub fn indexonly(&self) -> bool {
self == &AccessPerm::Inherit self == &AccessPerm::IndexOnly
} }
pub fn readwrite(&self) -> bool { pub fn readwrite(&self) -> bool {
self == &AccessPerm::ReadWrite self == &AccessPerm::ReadWrite
} }
pub fn forbidden(&self) -> bool {
self == &AccessPerm::Forbidden
}
} }
pub fn www_authenticate(res: &mut Response, args: &Args) -> Result<()> { pub fn www_authenticate(res: &mut Response, args: &Args) -> Result<()> {
@@ -295,7 +287,7 @@ pub fn www_authenticate(res: &mut Response, args: &Args) -> Result<()> {
pub fn get_auth_user(authorization: &HeaderValue) -> Option<String> { pub fn get_auth_user(authorization: &HeaderValue) -> Option<String> {
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") { if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?; let value: Vec<u8> = STANDARD.decode(value).ok()?;
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect(); let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
Some(parts[0].to_string()) Some(parts[0].to_string())
} else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") { } else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
@@ -314,7 +306,7 @@ pub fn check_auth(
auth_pass: &str, auth_pass: &str,
) -> Option<()> { ) -> Option<()> {
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") { if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?; let value: Vec<u8> = STANDARD.decode(value).ok()?;
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect(); let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
if parts[0] != auth_user { if parts[0] != auth_user {
@@ -437,6 +429,8 @@ fn is_readonly_method(method: &Method) -> bool {
|| method == Method::OPTIONS || method == Method::OPTIONS
|| method == Method::HEAD || method == Method::HEAD
|| method.as_str() == "PROPFIND" || method.as_str() == "PROPFIND"
|| method.as_str() == "CHECKAUTH"
|| method.as_str() == "LOGOUT"
} }
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> { fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
@@ -576,7 +570,6 @@ mod tests {
paths.add("/dir1", AccessPerm::ReadWrite); paths.add("/dir1", AccessPerm::ReadWrite);
paths.add("/dir2/dir21", AccessPerm::ReadWrite); paths.add("/dir2/dir21", AccessPerm::ReadWrite);
paths.add("/dir2/dir21/dir211", AccessPerm::ReadOnly); paths.add("/dir2/dir21/dir211", AccessPerm::ReadOnly);
paths.add("/dir2/dir21/dir212", AccessPerm::Forbidden);
paths.add("/dir2/dir22", AccessPerm::ReadOnly); paths.add("/dir2/dir22", AccessPerm::ReadOnly);
paths.add("/dir2/dir22/dir221", AccessPerm::ReadWrite); paths.add("/dir2/dir22/dir221", AccessPerm::ReadWrite);
paths.add("/dir2/dir23/dir231", AccessPerm::ReadWrite); paths.add("/dir2/dir23/dir231", AccessPerm::ReadWrite);
@@ -621,6 +614,5 @@ mod tests {
Some(AccessPaths::new(AccessPerm::ReadOnly)) Some(AccessPaths::new(AccessPerm::ReadOnly))
); );
assert_eq!(paths.find("dir2/dir21/dir211/file", true), None); assert_eq!(paths.find("dir2/dir21/dir211/file", true), None);
assert_eq!(paths.find("dir2/dir21/dir212", false), None);
} }
} }

View File

@@ -1,8 +1,14 @@
use anyhow::{Context, Result};
use chrono::{Local, SecondsFormat}; use chrono::{Local, SecondsFormat};
use log::{Level, Metadata, Record}; use log::{Level, LevelFilter, Metadata, Record};
use log::{LevelFilter, SetLoggerError}; use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
struct SimpleLogger; struct SimpleLogger {
file: Option<Mutex<File>>,
}
impl log::Log for SimpleLogger { impl log::Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool { fn enabled(&self, metadata: &Metadata) -> bool {
@@ -12,10 +18,20 @@ impl log::Log for SimpleLogger {
fn log(&self, record: &Record) { fn log(&self, record: &Record) {
if self.enabled(record.metadata()) { if self.enabled(record.metadata()) {
let timestamp = Local::now().to_rfc3339_opts(SecondsFormat::Secs, true); let timestamp = Local::now().to_rfc3339_opts(SecondsFormat::Secs, true);
if record.level() < Level::Info { let text = format!("{} {} - {}", timestamp, record.level(), record.args());
eprintln!("{} {} - {}", timestamp, record.level(), record.args()); match &self.file {
} else { Some(file) => {
println!("{} {} - {}", timestamp, record.level(), record.args()); if let Ok(mut file) = file.lock() {
let _ = writeln!(file, "{text}");
}
}
None => {
if record.level() < Level::Info {
eprintln!("{text}");
} else {
println!("{text}");
}
}
} }
} }
} }
@@ -23,8 +39,23 @@ impl log::Log for SimpleLogger {
fn flush(&self) {} fn flush(&self) {}
} }
static LOGGER: SimpleLogger = SimpleLogger; pub fn init(log_file: Option<PathBuf>) -> Result<()> {
let file = match log_file {
pub fn init() -> Result<(), SetLoggerError> { None => None,
log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Info)) Some(log_file) => {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
.with_context(|| {
format!("Failed to open the log file at '{}'", log_file.display())
})?;
Some(Mutex::new(file))
}
};
let logger = SimpleLogger { file };
log::set_boxed_logger(Box::new(logger))
.map(|_| log::set_max_level(LevelFilter::Info))
.with_context(|| "Failed to init logger")?;
Ok(())
} }

View File

@@ -29,13 +29,14 @@ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc,
}; };
use std::time::Duration;
use tokio::time::timeout;
use tokio::{net::TcpListener, task::JoinHandle}; use tokio::{net::TcpListener, task::JoinHandle};
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use tokio_rustls::{rustls::ServerConfig, TlsAcceptor}; use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
logger::init().map_err(|e| anyhow!("Failed to init logger, {e}"))?;
let cmd = build_cli(); let cmd = build_cli();
let matches = cmd.get_matches(); let matches = cmd.get_matches();
if let Some(generator) = matches.get_one::<Shell>("completions") { if let Some(generator) = matches.get_one::<Shell>("completions") {
@@ -43,9 +44,12 @@ async fn main() -> Result<()> {
print_completions(*generator, &mut cmd); print_completions(*generator, &mut cmd);
return Ok(()); return Ok(());
} }
let args = Args::parse(matches)?; let mut args = Args::parse(matches)?;
logger::init(args.log_file.clone()).map_err(|e| anyhow!("Failed to init logger, {e}"))?;
let (new_addrs, print_addrs) = check_addrs(&args)?;
args.addrs = new_addrs;
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let listening = print_listening(&args)?; let listening = print_listening(&args, &print_addrs)?;
let handles = serve(args, running.clone())?; let handles = serve(args, running.clone())?;
println!("{listening}"); println!("{listening}");
@@ -89,12 +93,19 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let config = Arc::new(config); let config = Arc::new(config);
let tls_accepter = TlsAcceptor::from(config); let tls_accepter = TlsAcceptor::from(config);
let handshake_timeout = Duration::from_secs(10);
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
loop { loop {
let (cnx, addr) = listener.accept().await.unwrap(); let Ok((stream, addr)) = listener.accept().await else {
let Ok(stream) = tls_accepter.accept(cnx).await else { continue;
warn!("During cls handshake connection from {}", addr); };
let Some(stream) =
timeout(handshake_timeout, tls_accepter.accept(stream))
.await
.ok()
.and_then(|v| v.ok())
else {
continue; continue;
}; };
let stream = TokioIo::new(stream); let stream = TokioIo::new(stream);
@@ -111,8 +122,10 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
(None, None) => { (None, None) => {
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
loop { loop {
let (cnx, addr) = listener.accept().await.unwrap(); let Ok((stream, addr)) = listener.accept().await else {
let stream = TokioIo::new(cnx); continue;
};
let stream = TokioIo::new(stream);
tokio::spawn(handle_stream( tokio::spawn(handle_stream(
server_handle.clone(), server_handle.clone(),
stream, stream,
@@ -137,8 +150,10 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
.with_context(|| format!("Failed to bind `{}`", path.display()))?; .with_context(|| format!("Failed to bind `{}`", path.display()))?;
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
loop { loop {
let (cnx, _) = listener.accept().await.unwrap(); let Ok((stream, _addr)) = listener.accept().await else {
let stream = TokioIo::new(cnx); continue;
};
let stream = TokioIo::new(stream);
tokio::spawn(handle_stream(server_handle.clone(), stream, None)); tokio::spawn(handle_stream(server_handle.clone(), stream, None));
} }
}); });
@@ -158,18 +173,15 @@ where
let hyper_service = let hyper_service =
service_fn(move |request: Request<Incoming>| handle.clone().call(request, addr)); service_fn(move |request: Request<Incoming>| handle.clone().call(request, addr));
let ret = Builder::new(TokioExecutor::new()) match Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(stream, hyper_service) .serve_connection_with_upgrades(stream, hyper_service)
.await; .await
{
if let Err(err) = ret { Ok(()) => {}
let scope = match addr { Err(_err) => {
Some(addr) => format!(" from {}", addr), // This error only appears when the client doesn't send a request and terminate the connection.
None => String::new(), //
}; // If client sends one request then terminate connection whenever, it doesn't appear.
match err.downcast_ref::<std::io::Error>() {
Some(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {}
_ => warn!("Serving connection{}: {}", scope, err),
} }
} }
} }
@@ -189,42 +201,64 @@ fn create_listener(addr: SocketAddr) -> Result<TcpListener> {
Ok(listener) Ok(listener)
} }
fn print_listening(args: &Args) -> Result<String> { fn check_addrs(args: &Args) -> Result<(Vec<BindAddr>, Vec<BindAddr>)> {
let mut output = String::new(); let mut new_addrs = vec![];
let mut bind_addrs = vec![]; let mut print_addrs = vec![];
let (mut ipv4, mut ipv6) = (false, false); let (ipv4_addrs, ipv6_addrs) = interface_addrs()?;
for bind_addr in args.addrs.iter() { for bind_addr in args.addrs.iter() {
match bind_addr { match bind_addr {
BindAddr::Address(ip) => { BindAddr::Address(ip) => match &ip {
if ip.is_unspecified() { IpAddr::V4(_) => {
if ip.is_ipv6() { if !ipv4_addrs.is_empty() {
ipv6 = true; new_addrs.push(bind_addr.clone());
} else { if ip.is_unspecified() {
ipv4 = true; print_addrs.extend(ipv4_addrs.clone());
} else {
print_addrs.push(bind_addr.clone());
}
} }
} else {
bind_addrs.push(bind_addr.clone());
} }
} IpAddr::V6(_) => {
_ => bind_addrs.push(bind_addr.clone()), if !ipv6_addrs.is_empty() {
} new_addrs.push(bind_addr.clone());
} if ip.is_unspecified() {
if ipv4 || ipv6 { print_addrs.extend(ipv6_addrs.clone());
let ifaces = } else {
if_addrs::get_if_addrs().with_context(|| "Failed to get local interface addresses")?; print_addrs.push(bind_addr.clone())
for iface in ifaces.into_iter() { }
let local_ip = iface.ip(); }
if ipv4 && local_ip.is_ipv4() { }
bind_addrs.push(BindAddr::Address(local_ip)) },
} _ => {
if ipv6 && local_ip.is_ipv6() { new_addrs.push(bind_addr.clone());
bind_addrs.push(BindAddr::Address(local_ip)) print_addrs.push(bind_addr.clone())
} }
} }
} }
bind_addrs.sort_unstable(); print_addrs.sort_unstable();
let urls = bind_addrs Ok((new_addrs, print_addrs))
.into_iter() }
fn interface_addrs() -> Result<(Vec<BindAddr>, Vec<BindAddr>)> {
let (mut ipv4_addrs, mut ipv6_addrs) = (vec![], vec![]);
let ifaces =
if_addrs::get_if_addrs().with_context(|| "Failed to get local interface addresses")?;
for iface in ifaces.into_iter() {
let ip = iface.ip();
if ip.is_ipv4() {
ipv4_addrs.push(BindAddr::Address(ip))
}
if ip.is_ipv6() {
ipv6_addrs.push(BindAddr::Address(ip))
}
}
Ok((ipv4_addrs, ipv6_addrs))
}
fn print_listening(args: &Args, print_addrs: &[BindAddr]) -> Result<String> {
let mut output = String::new();
let urls = print_addrs
.iter()
.map(|bind_addr| match bind_addr { .map(|bind_addr| match bind_addr {
BindAddr::Address(addr) => { BindAddr::Address(addr) => {
let addr = match addr { let addr = match addr {

View File

@@ -10,32 +10,34 @@ use crate::Args;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_zip::{tokio::write::ZipFileWriter, Compression, ZipDateTime, ZipEntryBuilder}; use async_zip::{tokio::write::ZipFileWriter, Compression, ZipDateTime, ZipEntryBuilder};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use bytes::Bytes; use bytes::Bytes;
use chrono::{LocalResult, TimeZone, Utc}; use chrono::{LocalResult, TimeZone, Utc};
use futures_util::{pin_mut, TryStreamExt}; use futures_util::{pin_mut, TryStreamExt};
use headers::{ use headers::{
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl, AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl,
ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfModifiedSince, IfNoneMatch, ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfMatch, IfModifiedSince,
IfRange, LastModified, Range, IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range,
}; };
use http_body_util::{combinators::BoxBody, BodyExt, StreamBody}; use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
use hyper::body::Frame; use hyper::body::Frame;
use hyper::{ use hyper::{
body::Incoming, body::Incoming,
header::{ header::{
HeaderValue, AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE, HeaderValue, AUTHORIZATION, CONNECTION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE,
CONTENT_TYPE, RANGE, CONTENT_TYPE, RANGE,
}, },
Method, StatusCode, Uri, Method, StatusCode, Uri,
}; };
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256};
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Ordering; 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::{Component, Path, PathBuf};
use std::sync::atomic::{self, AtomicBool}; use std::sync::atomic::{self, AtomicBool};
use std::sync::Arc; use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
@@ -71,7 +73,7 @@ pub struct Server {
impl Server { impl Server {
pub fn init(args: Args, running: Arc<AtomicBool>) -> Result<Self> { pub fn init(args: Args, running: Arc<AtomicBool>) -> Result<Self> {
let assets_prefix = format!("{}__dufs_v{}_", args.uri_prefix, env!("CARGO_PKG_VERSION")); let assets_prefix = format!("__dufs_v{}__/", env!("CARGO_PKG_VERSION"));
let single_file_req_paths = if args.path_is_file { let single_file_req_paths = if args.path_is_file {
vec![ vec![
args.uri_prefix.to_string(), args.uri_prefix.to_string(),
@@ -106,12 +108,18 @@ impl Server {
let uri = req.uri().clone(); let uri = req.uri().clone();
let assets_prefix = &self.assets_prefix; let assets_prefix = &self.assets_prefix;
let enable_cors = self.args.enable_cors; let enable_cors = self.args.enable_cors;
let is_microsoft_webdav = req
.headers()
.get("user-agent")
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("Microsoft-WebDAV-MiniRedir/"))
.unwrap_or_default();
let mut http_log_data = self.args.http_logger.data(&req); 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());
} }
let mut res = match self.clone().handle(req).await { let mut res = match self.clone().handle(req, is_microsoft_webdav).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) {
@@ -131,33 +139,49 @@ impl Server {
} }
}; };
if is_microsoft_webdav {
// microsoft webdav requires this.
res.headers_mut()
.insert(CONNECTION, HeaderValue::from_static("close"));
}
if enable_cors { if enable_cors {
add_cors(&mut res); add_cors(&mut res);
} }
Ok(res) Ok(res)
} }
pub async fn handle(self: Arc<Self>, req: Request) -> Result<Response> { pub async fn handle(
self: Arc<Self>,
req: Request,
is_microsoft_webdav: bool,
) -> Result<Response> {
let mut res = Response::default(); let mut res = Response::default();
let req_path = req.uri().path(); let req_path = req.uri().path();
let headers = req.headers(); let headers = req.headers();
let method = req.method().clone(); let method = req.method().clone();
if method == Method::GET && self.handle_assets(req_path, headers, &mut res).await? {
return Ok(res);
}
let authorization = headers.get(AUTHORIZATION);
let relative_path = match self.resolve_path(req_path) { let relative_path = match self.resolve_path(req_path) {
Some(v) => v, Some(v) => v,
None => { None => {
status_forbid(&mut res); status_bad_request(&mut res, "Invalid Path");
return Ok(res); return Ok(res);
} }
}; };
let guard = self.args.auth.guard(&relative_path, &method, authorization); if method == Method::GET
&& self
.handle_assets(&relative_path, headers, &mut res)
.await?
{
return Ok(res);
}
let authorization = headers.get(AUTHORIZATION);
let guard =
self.args
.auth
.guard(&relative_path, &method, authorization, is_microsoft_webdav);
let (user, access_paths) = match guard { let (user, access_paths) = match guard {
(None, None) => { (None, None) => {
@@ -176,7 +200,16 @@ impl Server {
.map(|(k, v)| (k.to_string(), v.to_string())) .map(|(k, v)| (k.to_string(), v.to_string()))
.collect(); .collect();
if method.as_str() == "WRITEABLE" { if method.as_str() == "CHECKAUTH" {
match user.clone() {
Some(user) => {
*res.body_mut() = body_full(user);
}
None => self.auth_reject(&mut res)?,
}
return Ok(res);
} else if method.as_str() == "LOGOUT" {
self.auth_reject(&mut res)?;
return Ok(res); return Ok(res);
} }
@@ -227,7 +260,7 @@ impl Server {
Method::GET | Method::HEAD => { Method::GET | Method::HEAD => {
if is_dir { if is_dir {
if render_try_index { if render_try_index {
if allow_archive && query_params.contains_key("zip") { if allow_archive && has_query_flag(&query_params, "zip") {
if !allow_archive { if !allow_archive {
status_not_found(&mut res); status_not_found(&mut res);
return Ok(res); return Ok(res);
@@ -267,7 +300,7 @@ impl Server {
&mut res, &mut res,
) )
.await?; .await?;
} else if query_params.contains_key("zip") { } else if has_query_flag(&query_params, "zip") {
if !allow_archive { if !allow_archive {
status_not_found(&mut res); status_not_found(&mut res);
return Ok(res); return Ok(res);
@@ -297,12 +330,14 @@ impl Server {
.await?; .await?;
} }
} else if is_file { } else if is_file {
if query_params.contains_key("edit") { if has_query_flag(&query_params, "edit") {
self.handle_deal_file(path, DataKind::Edit, head_only, user, &mut res) self.handle_edit_file(path, DataKind::Edit, head_only, user, &mut res)
.await?; .await?;
} else if query_params.contains_key("view") { } else if has_query_flag(&query_params, "view") {
self.handle_deal_file(path, DataKind::View, head_only, user, &mut res) self.handle_edit_file(path, DataKind::View, head_only, user, &mut res)
.await?; .await?;
} else if has_query_flag(&query_params, "hash") {
self.handle_hash_file(path, head_only, &mut res).await?;
} else { } else {
self.handle_send_file(path, headers, head_only, &mut res) self.handle_send_file(path, headers, head_only, &mut res)
.await?; .await?;
@@ -375,7 +410,7 @@ impl Server {
"PROPFIND" => { "PROPFIND" => {
if is_dir { if is_dir {
let access_paths = let access_paths =
if access_paths.perm().inherit() && authorization.is_none() { if access_paths.perm().indexonly() && authorization.is_none() {
// see https://github.com/sigoden/dufs/issues/229 // see https://github.com/sigoden/dufs/issues/229
AccessPaths::new(AccessPerm::ReadOnly) AccessPaths::new(AccessPerm::ReadOnly)
} else { } else {
@@ -623,7 +658,7 @@ impl Server {
) -> Result<()> { ) -> Result<()> {
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE); let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
let filename = try_get_file_name(path)?; let filename = try_get_file_name(path)?;
set_content_diposition(res, false, &format!("{}.zip", filename))?; set_content_disposition(res, false, &format!("{}.zip", filename))?;
res.headers_mut() res.headers_mut()
.insert("content-type", HeaderValue::from_static("application/zip")); .insert("content-type", HeaderValue::from_static("application/zip"));
if head_only { if head_only {
@@ -713,7 +748,12 @@ impl Server {
match self.args.assets.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?; if path.exists() {
self.handle_send_file(&path, headers, false, res).await?;
} else {
status_not_found(res);
return Ok(true);
}
} }
None => match name { None => match name {
"index.js" => { "index.js" => {
@@ -766,18 +806,29 @@ impl Server {
let size = meta.len(); let size = meta.len();
let mut use_range = true; let mut use_range = true;
if let Some((etag, last_modified)) = extract_cache_headers(&meta) { if let Some((etag, last_modified)) = extract_cache_headers(&meta) {
let cached = { if let Some(if_unmodified_since) = headers.typed_get::<IfUnmodifiedSince>() {
if let Some(if_none_match) = headers.typed_get::<IfNoneMatch>() { if !if_unmodified_since.precondition_passes(last_modified.into()) {
!if_none_match.precondition_passes(&etag) *res.status_mut() = StatusCode::PRECONDITION_FAILED;
} else if let Some(if_modified_since) = headers.typed_get::<IfModifiedSince>() { return Ok(());
!if_modified_since.is_modified(last_modified.into()) }
} else { }
false if let Some(if_match) = headers.typed_get::<IfMatch>() {
if !if_match.precondition_passes(&etag) {
*res.status_mut() = StatusCode::PRECONDITION_FAILED;
return Ok(());
}
}
if let Some(if_modified_since) = headers.typed_get::<IfModifiedSince>() {
if !if_modified_since.is_modified(last_modified.into()) {
*res.status_mut() = StatusCode::NOT_MODIFIED;
return Ok(());
}
}
if let Some(if_none_match) = headers.typed_get::<IfNoneMatch>() {
if !if_none_match.precondition_passes(&etag) {
*res.status_mut() = StatusCode::NOT_MODIFIED;
return Ok(());
} }
};
if cached {
*res.status_mut() = StatusCode::NOT_MODIFIED;
return Ok(());
} }
res.headers_mut().typed_insert(last_modified); res.headers_mut().typed_insert(last_modified);
@@ -811,7 +862,7 @@ impl Server {
); );
let filename = try_get_file_name(path)?; let filename = try_get_file_name(path)?;
set_content_diposition(res, true, filename)?; set_content_disposition(res, true, filename)?;
res.headers_mut().typed_insert(AcceptRanges::bytes()); res.headers_mut().typed_insert(AcceptRanges::bytes());
@@ -860,7 +911,7 @@ impl Server {
Ok(()) Ok(())
} }
async fn handle_deal_file( async fn handle_edit_file(
&self, &self,
path: &Path, path: &Path,
kind: DataKind, kind: DataKind,
@@ -890,10 +941,32 @@ 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));
let index_data = STANDARD.encode(serde_json::to_string(&data)?);
let output = self let output = self
.html .html
.replace("__ASSETS_PREFIX__", &self.assets_prefix) .replace(
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?); "__ASSETS_PREFIX__",
&format!("{}{}", self.args.uri_prefix, self.assets_prefix),
)
.replace("__INDEX_DATA__", &index_data);
res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64));
if head_only {
return Ok(());
}
*res.body_mut() = body_full(output);
Ok(())
}
async fn handle_hash_file(
&self,
path: &Path,
head_only: bool,
res: &mut Response,
) -> Result<()> {
let output = sha256_file(path).await?;
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
res.headers_mut() res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64)); .typed_insert(ContentLength(output.as_bytes().len() as u64));
if head_only { if head_only {
@@ -912,9 +985,10 @@ impl Server {
) -> Result<()> { ) -> Result<()> {
let depth: u32 = match headers.get("depth") { let depth: u32 = match headers.get("depth") {
Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) { Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
Some(v) => v, Some(0) => 0,
None => { Some(1) => 1,
status_bad_request(res, ""); _ => {
status_bad_request(res, "Invalid depth: only 0 and 1 are allowed.");
return Ok(()); return Ok(());
} }
}, },
@@ -924,7 +998,7 @@ impl Server {
Some(v) => vec![v], Some(v) => vec![v],
None => vec![], None => vec![],
}; };
if depth != 0 { if depth == 1 {
match self match self
.list_dir(path, &self.args.serve_path, access_paths) .list_dir(path, &self.args.serve_path, access_paths)
.await .await
@@ -1069,7 +1143,7 @@ impl Server {
} else { } else {
paths.sort_by(|v1, v2| v1.sort_by_name(v2)) paths.sort_by(|v1, v2| v1.sort_by_name(v2))
} }
if query_params.contains_key("simple") { if has_query_flag(query_params, "simple") {
let output = paths let output = paths
.into_iter() .into_iter()
.map(|v| { .map(|v| {
@@ -1109,16 +1183,21 @@ impl Server {
user, user,
paths, paths,
}; };
let output = if query_params.contains_key("json") { let output = if has_query_flag(query_params, "json") {
res.headers_mut() res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON)); .typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
serde_json::to_string_pretty(&data)? serde_json::to_string_pretty(&data)?
} else { } else {
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));
let index_data = STANDARD.encode(serde_json::to_string(&data)?);
self.html self.html
.replace("__ASSETS_PREFIX__", &self.assets_prefix) .replace(
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?) "__ASSETS_PREFIX__",
&format!("{}{}", self.args.uri_prefix, self.assets_prefix),
)
.replace("__INDEX_DATA__", &index_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));
@@ -1153,18 +1232,13 @@ impl Server {
fn extract_dest(&self, req: &Request, res: &mut Response) -> Option<PathBuf> { fn extract_dest(&self, req: &Request, res: &mut Response) -> Option<PathBuf> {
let headers = req.headers(); let headers = req.headers();
let dest_path = match self.extract_destination_header(headers) { let dest_path = match self
.extract_destination_header(headers)
.and_then(|dest| self.resolve_path(&dest))
{
Some(dest) => dest, Some(dest) => dest,
None => { None => {
*res.status_mut() = StatusCode::BAD_REQUEST; status_bad_request(res, "Invalid Destination");
return None;
}
};
let relative_path = match self.resolve_path(&dest_path) {
Some(v) => v,
None => {
*res.status_mut() = StatusCode::BAD_REQUEST;
return None; return None;
} }
}; };
@@ -1173,7 +1247,7 @@ impl Server {
let guard = self let guard = self
.args .args
.auth .auth
.guard(&relative_path, req.method(), authorization); .guard(&dest_path, req.method(), authorization, false);
match guard { match guard {
(_, Some(_)) => {} (_, Some(_)) => {}
@@ -1183,7 +1257,7 @@ impl Server {
} }
}; };
let dest = match self.join_path(&relative_path) { let dest = match self.join_path(&dest_path) {
Some(dest) => dest, Some(dest) => dest,
None => { None => {
*res.status_mut() = StatusCode::BAD_REQUEST; *res.status_mut() = StatusCode::BAD_REQUEST;
@@ -1201,13 +1275,30 @@ impl Server {
} }
fn resolve_path(&self, path: &str) -> Option<String> { fn resolve_path(&self, path: &str) -> Option<String> {
let path = path.trim_matches('/');
let path = decode_uri(path)?; let path = decode_uri(path)?;
let prefix = self.args.path_prefix.as_str(); let path = path.trim_matches('/');
if prefix == "/" { let mut parts = vec![];
return Some(path.to_string()); for comp in Path::new(path).components() {
if let Component::Normal(v) = comp {
let v = v.to_string_lossy();
if cfg!(windows) {
let chars: Vec<char> = v.chars().collect();
if chars.len() == 2 && chars[1] == ':' && chars[0].is_ascii_alphabetic() {
return None;
}
}
parts.push(v);
} else {
return None;
}
} }
path.strip_prefix(prefix.trim_start_matches('/')) let new_path = parts.join("/");
let path_prefix = self.args.path_prefix.as_str();
if path_prefix.is_empty() {
return Some(new_path);
}
new_path
.strip_prefix(path_prefix.trim_start_matches('/'))
.map(|v| v.trim_matches('/').to_string()) .map(|v| v.trim_matches('/').to_string())
} }
@@ -1230,7 +1321,7 @@ impl Server {
access_paths: AccessPaths, access_paths: AccessPaths,
) -> Result<Vec<PathItem>> { ) -> Result<Vec<PathItem>> {
let mut paths: Vec<PathItem> = vec![]; let mut paths: Vec<PathItem> = vec![];
if access_paths.perm().inherit() { if access_paths.perm().indexonly() {
for name in access_paths.child_names() { for name in access_paths.child_names() {
let entry_path = entry_path.join(name); let entry_path = entry_path.join(name);
self.add_pathitem(&mut paths, base_path, &entry_path).await; self.add_pathitem(&mut paths, base_path, &entry_path).await;
@@ -1335,7 +1426,7 @@ impl PathItem {
pub fn to_dav_xml(&self, prefix: &str) -> String { pub fn to_dav_xml(&self, prefix: &str) -> String {
let mtime = match Utc.timestamp_millis_opt(self.mtime as i64) { let mtime = match Utc.timestamp_millis_opt(self.mtime as i64) {
LocalResult::Single(v) => v.to_rfc2822(), LocalResult::Single(v) => format!("{}", v.format("%a, %d %b %Y %H:%M:%S GMT")),
_ => String::new(), _ => String::new(),
}; };
let mut href = encode_uri(&format!("{}{}", prefix, &self.name)); let mut href = encode_uri(&format!("{}{}", prefix, &self.name));
@@ -1508,7 +1599,6 @@ async fn zip_dir<W: AsyncWrite + Unpin>(
) -> Result<()> { ) -> Result<()> {
let mut writer = ZipFileWriter::with_tokio(writer); let mut writer = ZipFileWriter::with_tokio(writer);
let hidden = Arc::new(hidden.to_vec()); let hidden = Arc::new(hidden.to_vec());
let hidden = hidden.clone();
let dir_clone = dir.to_path_buf(); let dir_clone = dir.to_path_buf();
let zip_paths = tokio::task::spawn_blocking(move || { let zip_paths = tokio::task::spawn_blocking(move || {
let mut paths: Vec<PathBuf> = vec![]; let mut paths: Vec<PathBuf> = vec![];
@@ -1599,7 +1689,7 @@ fn status_bad_request(res: &mut Response, body: &str) {
} }
} }
fn set_content_diposition(res: &mut Response, inline: bool, filename: &str) -> Result<()> { fn set_content_disposition(res: &mut Response, inline: bool, filename: &str) -> Result<()> {
let kind = if inline { "inline" } else { "attachment" }; let kind = if inline { "inline" } else { "attachment" };
let filename: String = filename let filename: String = filename
.chars() .chars()
@@ -1638,12 +1728,12 @@ fn is_hidden(hidden: &[String], file_name: &str, is_dir_type: bool) -> bool {
fn set_webdav_headers(res: &mut Response) { fn set_webdav_headers(res: &mut Response) {
res.headers_mut().insert( res.headers_mut().insert(
"Allow", "Allow",
HeaderValue::from_static("GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE"), HeaderValue::from_static(
); "GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE,CHECKAUTH,LOGOUT",
res.headers_mut().insert( ),
"DAV",
HeaderValue::from_static("1, 2, 3, sabredav-partialupdate"),
); );
res.headers_mut()
.insert("DAV", HeaderValue::from_static("1, 2, 3"));
} }
async fn get_content_type(path: &Path) -> Result<String> { async fn get_content_type(path: &Path) -> Result<String> {
@@ -1682,7 +1772,7 @@ fn parse_upload_offset(headers: &HeaderMap<HeaderValue>, size: u64) -> Result<Op
Some(v) => v, Some(v) => v,
None => return Ok(None), None => return Ok(None),
}; };
let err = || anyhow!("Invalid X-Updage-Range header"); let err = || anyhow!("Invalid X-Update-Range Header");
let value = value.to_str().map_err(|_| err())?; let value = value.to_str().map_err(|_| err())?;
if value == "append" { if value == "append" {
return Ok(Some(size)); return Ok(Some(size));
@@ -1690,3 +1780,27 @@ fn parse_upload_offset(headers: &HeaderMap<HeaderValue>, size: u64) -> Result<Op
let (start, _) = parse_range(value, size).ok_or_else(err)?; let (start, _) = parse_range(value, size).ok_or_else(err)?;
Ok(Some(start)) Ok(Some(start))
} }
async fn sha256_file(path: &Path) -> Result<String> {
let mut file = fs::File::open(path).await?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = file.read(&mut buffer).await?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let result = hasher.finalize();
Ok(format!("{:x}", result))
}
fn has_query_flag(query_params: &HashMap<String, String>, name: &str) -> bool {
query_params
.get(name)
.map(|v| v.is_empty())
.unwrap_or_default()
}

View File

@@ -11,10 +11,11 @@ use std::process::{Command, Stdio};
fn assets(server: TestServer) -> Result<(), Error> { fn assets(server: TestServer) -> Result<(), Error> {
let ver = env!("CARGO_PKG_VERSION"); let ver = env!("CARGO_PKG_VERSION");
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
let index_js = format!("/__dufs_v{ver}_index.js"); let index_js = format!("/__dufs_v{ver}__/index.js");
let index_css = format!("/__dufs_v{ver}_index.css"); let index_css = format!("/__dufs_v{ver}__/index.css");
let favicon_ico = format!("/__dufs_v{ver}_favicon.ico"); let favicon_ico = format!("/__dufs_v{ver}__/favicon.ico");
let text = resp.text()?; let text = resp.text()?;
println!("{text}");
assert!(text.contains(&format!(r#"href="{index_css}""#))); assert!(text.contains(&format!(r#"href="{index_css}""#)));
assert!(text.contains(&format!(r#"href="{favicon_ico}""#))); assert!(text.contains(&format!(r#"href="{favicon_ico}""#)));
assert!(text.contains(&format!(r#"src="{index_js}""#))); assert!(text.contains(&format!(r#"src="{index_js}""#)));
@@ -24,7 +25,7 @@ fn assets(server: TestServer) -> Result<(), Error> {
#[rstest] #[rstest]
fn asset_js(server: TestServer) -> Result<(), Error> { fn asset_js(server: TestServer) -> Result<(), Error> {
let url = format!( let url = format!(
"{}__dufs_v{}_index.js", "{}__dufs_v{}__/index.js",
server.url(), server.url(),
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
); );
@@ -40,7 +41,7 @@ fn asset_js(server: TestServer) -> Result<(), Error> {
#[rstest] #[rstest]
fn asset_css(server: TestServer) -> Result<(), Error> { fn asset_css(server: TestServer) -> Result<(), Error> {
let url = format!( let url = format!(
"{}__dufs_v{}_index.css", "{}__dufs_v{}__/index.css",
server.url(), server.url(),
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
); );
@@ -56,7 +57,7 @@ fn asset_css(server: TestServer) -> Result<(), Error> {
#[rstest] #[rstest]
fn asset_ico(server: TestServer) -> Result<(), Error> { fn asset_ico(server: TestServer) -> Result<(), Error> {
let url = format!( let url = format!(
"{}__dufs_v{}_favicon.ico", "{}__dufs_v{}__/favicon.ico",
server.url(), server.url(),
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
); );
@@ -70,9 +71,9 @@ fn asset_ico(server: TestServer) -> Result<(), Error> {
fn assets_with_prefix(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> { fn assets_with_prefix(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
let ver = env!("CARGO_PKG_VERSION"); let ver = env!("CARGO_PKG_VERSION");
let resp = reqwest::blocking::get(format!("{}xyz/", server.url()))?; let resp = reqwest::blocking::get(format!("{}xyz/", server.url()))?;
let index_js = format!("/xyz/__dufs_v{ver}_index.js"); let index_js = format!("/xyz/__dufs_v{ver}__/index.js");
let index_css = format!("/xyz/__dufs_v{ver}_index.css"); let index_css = format!("/xyz/__dufs_v{ver}__/index.css");
let favicon_ico = format!("/xyz/__dufs_v{ver}_favicon.ico"); let favicon_ico = format!("/xyz/__dufs_v{ver}__/favicon.ico");
let text = resp.text()?; let text = resp.text()?;
assert!(text.contains(&format!(r#"href="{index_css}""#))); assert!(text.contains(&format!(r#"href="{index_css}""#)));
assert!(text.contains(&format!(r#"href="{favicon_ico}""#))); assert!(text.contains(&format!(r#"href="{favicon_ico}""#)));
@@ -85,7 +86,7 @@ fn asset_js_with_prefix(
#[with(&["--path-prefix", "xyz"])] server: TestServer, #[with(&["--path-prefix", "xyz"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!( let url = format!(
"{}xyz/__dufs_v{}_index.js", "{}xyz/__dufs_v{}__/index.js",
server.url(), server.url(),
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
); );
@@ -114,7 +115,7 @@ fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let url = format!("http://localhost:{port}"); let url = format!("http://localhost:{port}");
let resp = reqwest::blocking::get(&url)?; let resp = reqwest::blocking::get(&url)?;
assert!(resp.text()?.starts_with(&format!( assert!(resp.text()?.starts_with(&format!(
"/__dufs_v{}_index.js;DATA", "/__dufs_v{}__/index.js;<template id=\"index-data\">",
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
))); )));
let resp = reqwest::blocking::get(&url)?; let resp = reqwest::blocking::get(&url)?;

View File

@@ -1,7 +1,8 @@
mod digest_auth_util;
mod fixtures; mod fixtures;
mod utils; mod utils;
use diqwest::blocking::WithDigestAuth; use digest_auth_util::send_with_digest_auth;
use fixtures::{server, Error, TestServer}; use fixtures::{server, Error, TestServer};
use indexmap::IndexSet; use indexmap::IndexSet;
use rstest::rstest; use rstest::rstest;
@@ -32,13 +33,30 @@ fn auth(#[case] server: TestServer, #[case] user: &str, #[case] pass: &str) -> R
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), user, pass)?;
.body(b"abc".to_vec())
.send_with_digest_auth(user, pass)?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
Ok(()) Ok(())
} }
#[rstest]
fn invalid_auth(
#[with(&["-a", "user:pass@/:rw", "-a", "@/", "-A"])] server: TestServer,
) -> Result<(), Error> {
let resp = fetch!(b"GET", server.url())
.basic_auth("user", Some("-"))
.send()?;
assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", server.url())
.basic_auth("-", Some("pass"))
.send()?;
assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", server.url())
.header("Authorization", "Basic Og==")
.send()?;
assert_eq!(resp.status(), 401);
Ok(())
}
const HASHED_PASSWORD_AUTH: &str = "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw"; // user:pass const HASHED_PASSWORD_AUTH: &str = "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw"; // user:pass
#[rstest] #[rstest]
@@ -48,13 +66,12 @@ fn auth_hashed_password(
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
if let Err(err) = fetch!(b"PUT", &url) if let Err(err) =
.body(b"abc".to_vec()) send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")
.send_with_digest_auth("user", "pass")
{ {
assert_eq!( assert_eq!(
format!("{err:?}"), err.to_string(),
r#"DigestAuth(MissingRequired("realm", "Basic realm=\"DUFS\""))"# r#"Missing "realm" in header: Basic realm="DUFS""#
); );
} }
let resp = fetch!(b"PUT", &url) let resp = fetch!(b"PUT", &url)
@@ -72,9 +89,7 @@ fn auth_and_public(
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
@@ -104,26 +119,38 @@ fn auth_check(
#[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"WRITEABLE", &url).send()?; let resp = fetch!(b"CHECKAUTH", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user2", "pass2")?; let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 200);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user2", "pass2")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
} }
#[rstest] #[rstest]
fn auth_compact_rules( fn auth_check2(
#[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"WRITEABLE", &url).send()?; let resp = fetch!(b"CHECKAUTH", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user2", "pass2")?; let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
assert_eq!(resp.status(), 403);
let resp = fetch!(b"WRITEABLE", &url).send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user2", "pass2")?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn auth_logout(
#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"LOGOUT", &url).send()?;
assert_eq!(resp.status(), 401);
let resp = send_with_digest_auth(fetch!(b"LOGOUT", &url), "user", "pass")?;
assert_eq!(resp.status(), 401);
Ok(()) Ok(())
} }
@@ -134,29 +161,10 @@ fn auth_readonly(
let url = format!("{}index.html", server.url()); let url = format!("{}index.html", server.url());
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", &url).send_with_digest_auth("user2", "pass2")?; let resp = send_with_digest_auth(fetch!(b"GET", &url), "user2", "pass2")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let url = format!("{}file1", server.url()); let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user2", "pass2")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user2", "pass2")?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn auth_forbidden(
#[with(&["--auth", "user:pass@/:rw,/dir1:-", "-A"])] server: TestServer,
) -> Result<(), Error> {
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201);
let url = format!("{}dir1/file1", server.url());
let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 403);
Ok(()) Ok(())
} }
@@ -169,13 +177,9 @@ fn auth_nest(
let url = format!("{}dir1/file1", server.url()); let url = format!("{}dir1/file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user3", "pass3")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user3", "pass3")?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
Ok(()) Ok(())
} }
@@ -217,9 +221,11 @@ fn auth_webdav_move(
) -> Result<(), Error> { ) -> Result<(), Error> {
let origin_url = format!("{}dir1/test.html", server.url()); let origin_url = format!("{}dir1/test.html", server.url());
let new_url = format!("{}test2.html", server.url()); let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"MOVE", &origin_url) let resp = send_with_digest_auth(
.header("Destination", &new_url) fetch!(b"MOVE", &origin_url).header("Destination", &new_url),
.send_with_digest_auth("user3", "pass3")?; "user3",
"pass3",
)?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 403);
Ok(()) Ok(())
} }
@@ -231,9 +237,11 @@ fn auth_webdav_copy(
) -> Result<(), Error> { ) -> Result<(), Error> {
let origin_url = format!("{}dir1/test.html", server.url()); let origin_url = format!("{}dir1/test.html", server.url());
let new_url = format!("{}test2.html", server.url()); let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"COPY", &origin_url) let resp = send_with_digest_auth(
.header("Destination", &new_url) fetch!(b"COPY", &origin_url).header("Destination", &new_url),
.send_with_digest_auth("user3", "pass3")?; "user3",
"pass3",
)?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 403);
Ok(()) Ok(())
} }
@@ -245,7 +253,7 @@ fn auth_path_prefix(
let url = format!("{}xyz/index.html", server.url()); let url = format!("{}xyz/index.html", server.url());
let resp = fetch!(b"GET", &url).send()?; let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"GET", &url), "user", "pass")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
Ok(()) Ok(())
} }
@@ -254,12 +262,15 @@ fn auth_path_prefix(
fn auth_partial_index( fn auth_partial_index(
#[with(&["--auth", "user:pass@/dir1:rw,/dir2:rw", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/dir1:rw,/dir2:rw", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let resp = fetch!(b"GET", server.url()).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"GET", server.url()), "user", "pass")?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?); let paths = utils::retrieve_index_paths(&resp.text()?);
assert_eq!(paths, IndexSet::from(["dir1/".into(), "dir2/".into()])); assert_eq!(paths, IndexSet::from(["dir1/".into(), "dir2/".into()]));
let resp = fetch!(b"GET", format!("{}?q={}", server.url(), "test.html")) let resp = send_with_digest_auth(
.send_with_digest_auth("user", "pass")?; fetch!(b"GET", format!("{}?q={}", server.url(), "test.html")),
"user",
"pass",
)?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?); let paths = utils::retrieve_index_paths(&resp.text()?);
assert_eq!( assert_eq!(
@@ -286,7 +297,7 @@ fn auth_propfind_dir(
#[with(&["--auth", "admin:admin@/:rw", "--auth", "user:pass@/dir-assets", "-A"])] #[with(&["--auth", "admin:admin@/:rw", "--auth", "user:pass@/dir-assets", "-A"])]
server: TestServer, server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", server.url()).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"PROPFIND", server.url()), "user", "pass")?;
assert_eq!(resp.status(), 207); assert_eq!(resp.status(), 207);
let body = resp.text()?; let body = resp.text()?;
assert!(body.contains("<D:href>/dir-assets/</D:href>")); assert!(body.contains("<D:href>/dir-assets/</D:href>"));
@@ -300,14 +311,14 @@ fn auth_data(
) -> Result<(), Error> { ) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?; let resp = reqwest::blocking::get(server.url())?;
let content = resp.text()?; let content = resp.text()?;
let json = utils::retrive_json(&content).unwrap(); let json = utils::retrieve_json(&content).unwrap();
assert_eq!(json["allow_delete"], serde_json::Value::Bool(false)); assert_eq!(json["allow_delete"], serde_json::Value::Bool(false));
assert_eq!(json["allow_upload"], serde_json::Value::Bool(false)); assert_eq!(json["allow_upload"], serde_json::Value::Bool(false));
let resp = fetch!(b"GET", server.url()) let resp = fetch!(b"GET", server.url())
.basic_auth("user", Some("pass")) .basic_auth("user", Some("pass"))
.send()?; .send()?;
let content = resp.text()?; let content = resp.text()?;
let json = utils::retrive_json(&content).unwrap(); let json = utils::retrieve_json(&content).unwrap();
assert_eq!(json["allow_delete"], serde_json::Value::Bool(true)); assert_eq!(json["allow_delete"], serde_json::Value::Bool(true));
assert_eq!(json["allow_upload"], serde_json::Value::Bool(true)); assert_eq!(json["allow_upload"], serde_json::Value::Bool(true));
Ok(()) Ok(())
@@ -318,15 +329,11 @@ fn auth_precedence(
#[with(&["--auth", "user:pass@/dir1:rw,/dir1/test.txt", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/dir1:rw,/dir1/test.txt", "-A"])] server: TestServer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let url = format!("{}dir1/test.txt", server.url()); let url = format!("{}dir1/test.txt", server.url());
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 403);
let url = format!("{}dir1/file1", server.url()); let url = format!("{}dir1/file1", server.url());
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")?;
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
Ok(()) Ok(())

80
tests/cache.rs Normal file
View File

@@ -0,0 +1,80 @@
mod fixtures;
mod utils;
use chrono::{DateTime, Duration};
use fixtures::{server, Error, TestServer};
use reqwest::header::{
HeaderName, ETAG, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE,
LAST_MODIFIED,
};
use reqwest::StatusCode;
use rstest::rstest;
#[rstest]
#[case(IF_UNMODIFIED_SINCE, Duration::days(1), StatusCode::OK)]
#[case(IF_UNMODIFIED_SINCE, Duration::days(0), StatusCode::OK)]
#[case(IF_UNMODIFIED_SINCE, Duration::days(-1), StatusCode::PRECONDITION_FAILED)]
#[case(IF_MODIFIED_SINCE, Duration::days(1), StatusCode::NOT_MODIFIED)]
#[case(IF_MODIFIED_SINCE, Duration::days(0), StatusCode::NOT_MODIFIED)]
#[case(IF_MODIFIED_SINCE, Duration::days(-1), StatusCode::OK)]
fn get_file_with_if_modified_since_condition(
#[case] header_condition: HeaderName,
#[case] duration_after_file_modified: Duration,
#[case] expected_code: StatusCode,
server: TestServer,
) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
let last_modified = resp
.headers()
.get(LAST_MODIFIED)
.and_then(|h| h.to_str().ok())
.and_then(|s| DateTime::parse_from_rfc2822(s).ok())
.expect("Received no valid last modified header");
let req_modified_time = (last_modified + duration_after_file_modified)
.format("%a, %d %b %Y %T GMT")
.to_string();
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
.header(header_condition, req_modified_time)
.send()?;
assert_eq!(resp.status(), expected_code);
Ok(())
}
fn same_etag(etag: &str) -> String {
etag.to_owned()
}
fn different_etag(etag: &str) -> String {
format!("{}1234", etag)
}
#[rstest]
#[case(IF_MATCH, same_etag, StatusCode::OK)]
#[case(IF_MATCH, different_etag, StatusCode::PRECONDITION_FAILED)]
#[case(IF_NONE_MATCH, same_etag, StatusCode::NOT_MODIFIED)]
#[case(IF_NONE_MATCH, different_etag, StatusCode::OK)]
fn get_file_with_etag_match(
#[case] header_condition: HeaderName,
#[case] etag_modifier: fn(&str) -> String,
#[case] expected_code: StatusCode,
server: TestServer,
) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
let etag = resp
.headers()
.get(ETAG)
.and_then(|h| h.to_str().ok())
.expect("Received no valid etag header");
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
.header(header_condition, etag_modifier(etag))
.send()?;
assert_eq!(resp.status(), expected_code);
Ok(())
}

View File

@@ -1,9 +1,10 @@
mod digest_auth_util;
mod fixtures; mod fixtures;
mod utils; mod utils;
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use assert_fs::TempDir; use assert_fs::TempDir;
use diqwest::blocking::WithDigestAuth; use digest_auth_util::send_with_digest_auth;
use fixtures::{port, tmpdir, wait_for_port, Error}; use fixtures::{port, tmpdir, wait_for_port, Error};
use rstest::rstest; use rstest::rstest;
use std::path::PathBuf; use std::path::PathBuf;
@@ -27,20 +28,18 @@ fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> {
assert_eq!(resp.status(), 401); assert_eq!(resp.status(), 401);
let url = format!("http://localhost:{port}/dufs/index.html"); let url = format!("http://localhost:{port}/dufs/index.html");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"GET", &url), "user", "pass")?;
assert_eq!(resp.text()?, "This is index.html"); assert_eq!(resp.text()?, "This is index.html");
let url = format!("http://localhost:{port}/dufs?simple"); let url = format!("http://localhost:{port}/dufs?simple");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?; let resp = send_with_digest_auth(fetch!(b"GET", &url), "user", "pass")?;
let text: String = resp.text().unwrap(); let text: String = resp.text().unwrap();
assert!(text.split('\n').any(|c| c == "dir1/")); assert!(text.split('\n').any(|c| c == "dir1/"));
assert!(!text.split('\n').any(|c| c == "dir3/")); assert!(!text.split('\n').any(|c| c == "dir3/"));
assert!(!text.split('\n').any(|c| c == "test.txt")); assert!(!text.split('\n').any(|c| c == "test.txt"));
let url = format!("http://localhost:{port}/dufs/dir1/upload.txt"); let url = format!("http://localhost:{port}/dufs/dir1/upload.txt");
let resp = fetch!(b"PUT", &url) let resp = send_with_digest_auth(fetch!(b"PUT", &url).body("Hello"), "user", "pass")?;
.body("Hello")
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
child.kill()?; child.kill()?;

91
tests/digest_auth_util.rs Normal file
View File

@@ -0,0 +1,91 @@
/// Refs https://github.dev/maoertel/diqwest/blob/main/src/blocking.rs
use anyhow::{anyhow, Result};
use digest_auth::{AuthContext, AuthorizationHeader, HttpMethod};
use hyper::{header::AUTHORIZATION, HeaderMap, StatusCode};
use reqwest::blocking::{RequestBuilder, Response};
use url::Position;
pub fn send_with_digest_auth(
request_builder: RequestBuilder,
username: &str,
password: &str,
) -> Result<Response> {
let first_response = try_clone_request_builder(&request_builder)?.send()?;
match first_response.status() {
StatusCode::UNAUTHORIZED => {
try_digest_auth(request_builder, first_response, username, password)
}
_ => Ok(first_response),
}
}
fn try_digest_auth(
request_builder: RequestBuilder,
first_response: Response,
username: &str,
password: &str,
) -> Result<Response> {
if let Some(answer) = get_answer(
&request_builder,
first_response.headers(),
username,
password,
)? {
return Ok(request_builder
.header(AUTHORIZATION, answer.to_header_string())
.send()?);
};
Ok(first_response)
}
fn try_clone_request_builder(request_builder: &RequestBuilder) -> Result<RequestBuilder> {
request_builder
.try_clone()
.ok_or_else(|| anyhow!("Request body must not be a stream"))
}
fn get_answer(
request_builder: &RequestBuilder,
first_response: &HeaderMap,
username: &str,
password: &str,
) -> Result<Option<AuthorizationHeader>> {
let answer = calculate_answer(request_builder, first_response, username, password);
match answer {
Ok(answer) => Ok(Some(answer)),
Err(error) => Err(error),
}
}
fn calculate_answer(
request_builder: &RequestBuilder,
headers: &HeaderMap,
username: &str,
password: &str,
) -> Result<AuthorizationHeader> {
let request = try_clone_request_builder(request_builder)?.build()?;
let path = &request.url()[Position::AfterPort..];
let method = HttpMethod::from(request.method().as_str());
let body = request.body().and_then(|b| b.as_bytes());
parse_digest_auth_header(headers, path, method, body, username, password)
}
fn parse_digest_auth_header(
header: &HeaderMap,
path: &str,
method: HttpMethod,
body: Option<&[u8]>,
username: &str,
password: &str,
) -> Result<AuthorizationHeader> {
let www_auth = header
.get("www-authenticate")
.ok_or_else(|| anyhow!("The header 'www-authenticate' is missing."))?
.to_str()?;
let context = AuthContext::new_with_method(username, password, path, body, method);
let mut prompt = digest_auth::parse(www_auth)?;
Ok(prompt.respond(&context)?)
}

View File

@@ -65,7 +65,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("__ASSETS_PREFIX__index.js;DATA = __INDEX_DATA__") .write_str("__ASSETS_PREFIX__index.js;<template id=\"index-data\">__INDEX_DATA__</template>")
.unwrap(); .unwrap();
} else { } else {
for file in FILES { for file in FILES {

View File

@@ -59,7 +59,7 @@ fn hidden_search_dir(#[case] server: TestServer, #[case] exist: bool) -> Result<
#[rstest] #[rstest]
#[case(server(&["--hidden", "hidden/"]), "dir4/", 1)] #[case(server(&["--hidden", "hidden/"]), "dir4/", 1)]
#[case(server(&["--hidden", "hidden"]), "dir4/", 0)] #[case(server(&["--hidden", "hidden"]), "dir4/", 0)]
fn hidden_dir_noly( fn hidden_dir_only(
#[case] server: TestServer, #[case] server: TestServer,
#[case] dir: &str, #[case] dir: &str,
#[case] count: usize, #[case] count: usize,

View File

@@ -4,7 +4,7 @@ mod utils;
use fixtures::{server, Error, TestServer, BIN_FILE}; use fixtures::{server, Error, TestServer, BIN_FILE};
use rstest::rstest; use rstest::rstest;
use serde_json::Value; use serde_json::Value;
use utils::retrive_edit_file; use utils::retrieve_edit_file;
#[rstest] #[rstest]
fn get_dir(server: TestServer) -> Result<(), Error> { fn get_dir(server: TestServer) -> Result<(), Error> {
@@ -189,6 +189,21 @@ fn head_file(server: TestServer) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[rstest]
fn hash_file(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}index.html?hash", server.url()))?;
assert_eq!(
resp.headers().get("content-type").unwrap(),
"text/html; charset=utf-8"
);
assert_eq!(resp.status(), 200);
assert_eq!(
resp.text()?,
"c8dd395e3202674b9512f7b7f956e0d96a8ba8f572e785b0d5413ab83766dbc4"
);
Ok(())
}
#[rstest] #[rstest]
fn get_file_404(server: TestServer) -> Result<(), Error> { fn get_file_404(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404", server.url()))?; let resp = reqwest::blocking::get(format!("{}404", server.url()))?;
@@ -223,7 +238,7 @@ fn get_file_newline_path(server: TestServer) -> Result<(), Error> {
fn get_file_edit(server: TestServer) -> Result<(), Error> { fn get_file_edit(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}index.html?edit", server.url())).send()?; let resp = fetch!(b"GET", format!("{}index.html?edit", server.url())).send()?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let editable = retrive_edit_file(&resp.text().unwrap()).unwrap(); let editable = retrieve_edit_file(&resp.text().unwrap()).unwrap();
assert!(editable); assert!(editable);
Ok(()) Ok(())
} }
@@ -232,7 +247,7 @@ fn get_file_edit(server: TestServer) -> Result<(), Error> {
fn get_file_edit_bin(server: TestServer) -> Result<(), Error> { fn get_file_edit_bin(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}{BIN_FILE}?edit", server.url())).send()?; let resp = fetch!(b"GET", format!("{}{BIN_FILE}?edit", server.url())).send()?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let editable = retrive_edit_file(&resp.text().unwrap()).unwrap(); let editable = retrieve_edit_file(&resp.text().unwrap()).unwrap();
assert!(!editable); assert!(!editable);
Ok(()) Ok(())
} }
@@ -250,12 +265,9 @@ fn options_dir(server: TestServer) -> Result<(), Error> {
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
assert_eq!( assert_eq!(
resp.headers().get("allow").unwrap(), resp.headers().get("allow").unwrap(),
"GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE" "GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE,CHECKAUTH,LOGOUT"
);
assert_eq!(
resp.headers().get("dav").unwrap(),
"1, 2, 3, sabredav-partialupdate"
); );
assert_eq!(resp.headers().get("dav").unwrap(), "1, 2, 3");
Ok(()) Ok(())
} }

View File

@@ -1,7 +1,8 @@
mod digest_auth_util;
mod fixtures; mod fixtures;
mod utils; mod utils;
use diqwest::blocking::WithDigestAuth; use digest_auth_util::send_with_digest_auth;
use fixtures::{port, tmpdir, wait_for_port, Error}; use fixtures::{port, tmpdir, wait_for_port, Error};
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
@@ -31,12 +32,12 @@ fn log_remote_user(
let stdout = child.stdout.as_mut().expect("Failed to get stdout"); let stdout = child.stdout.as_mut().expect("Failed to get stdout");
let req = fetch!(b"GET", &format!("http://localhost:{port}")); let req_builder = fetch!(b"GET", &format!("http://localhost:{port}"));
let resp = if is_basic { let resp = if is_basic {
req.basic_auth("user", Some("pass")).send()? req_builder.basic_auth("user", Some("pass")).send()?
} else { } else {
req.send_with_digest_auth("user", "pass")? send_with_digest_auth(req_builder, "user", "pass")?
}; };
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);

View File

@@ -53,7 +53,7 @@ fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Re
let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?; let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?;
assert_eq!(resp.text()?, "This is index.html"); assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?; let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?;
assert_eq!(resp.status(), 403); assert_eq!(resp.status(), 400);
child.kill()?; child.kill()?;
Ok(()) Ok(())

View File

@@ -1,3 +1,4 @@
use base64::{engine::general_purpose::STANDARD, Engine as _};
use indexmap::IndexSet; use indexmap::IndexSet;
use serde_json::Value; use serde_json::Value;
@@ -26,7 +27,7 @@ macro_rules! fetch {
#[allow(dead_code)] #[allow(dead_code)]
pub fn retrieve_index_paths(content: &str) -> IndexSet<String> { pub fn retrieve_index_paths(content: &str) -> IndexSet<String> {
let value = retrive_json(content).unwrap(); let value = retrieve_json(content).unwrap();
let paths = value let paths = value
.get("paths") .get("paths")
.unwrap() .unwrap()
@@ -47,8 +48,8 @@ pub fn retrieve_index_paths(content: &str) -> IndexSet<String> {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn retrive_edit_file(content: &str) -> Option<bool> { pub fn retrieve_edit_file(content: &str) -> Option<bool> {
let value = retrive_json(content)?; let value = retrieve_json(content).unwrap();
let value = value.get("editable").unwrap(); let value = value.get("editable").unwrap();
Some(value.as_bool().unwrap()) Some(value.as_bool().unwrap())
} }
@@ -60,10 +61,22 @@ pub fn encode_uri(v: &str) -> String {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn retrive_json(content: &str) -> Option<Value> { pub fn retrieve_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 start_tag = "<template id=\"index-data\">";
let line_col = line.find("DATA =").unwrap() + 6; let end_tag = "</template>";
let value: Value = line[line_col..].parse().unwrap();
let line = lines.iter().find(|v| v.contains(start_tag))?;
let start_index = line.find(start_tag)?;
let start_content_index = start_index + start_tag.len();
let end_index = line[start_content_index..].find(end_tag)?;
let end_content_index = start_content_index + end_index;
let value = &line[start_content_index..end_content_index];
let value = STANDARD.decode(value).ok()?;
let value = serde_json::from_slice(&value).ok()?;
Some(value) Some(value)
} }

View File

@@ -40,6 +40,17 @@ fn propfind_dir_depth0(server: TestServer) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[rstest]
fn propfind_dir_depth2(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}dir1", server.url()))
.header("depth", "2")
.send()?;
assert_eq!(resp.status(), 400);
let body = resp.text()?;
assert_eq!(body, "Invalid depth: only 0 and 1 are allowed.");
Ok(())
}
#[rstest] #[rstest]
fn propfind_404(server: TestServer) -> Result<(), Error> { fn propfind_404(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}404", server.url())).send()?; let resp = fetch!(b"PROPFIND", format!("{}404", server.url())).send()?;
@@ -49,7 +60,7 @@ fn propfind_404(server: TestServer) -> Result<(), Error> {
#[rstest] #[rstest]
fn propfind_double_slash(server: TestServer) -> Result<(), Error> { fn propfind_double_slash(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}/", server.url())).send()?; let resp = fetch!(b"PROPFIND", server.url()).send()?;
assert_eq!(resp.status(), 207); assert_eq!(resp.status(), 207);
Ok(()) Ok(())
} }