Compare commits

...
109 Commits
Author SHA1 Message Date
renovate[bot] db385c76b2 chore(deps): update all digest updates (#338) 2026-06-24 08:51:24 +02:00
renovate[bot] f9c771fdad chore(deps): update actions/checkout action to v7 (#339) 2026-06-24 08:51:15 +02:00
renovate[bot] 7cc3f8b4ae chore(deps): update all non-major dependencies (#336) 2026-06-13 07:11:02 +02:00
renovate[bot] 36063ce391 chore(deps): update all digest updates to 5f2de1a (#334) 2026-06-11 10:57:08 +02:00
renovate[bot] 6c3258a469 chore(deps): update all digest updates to e2ffffe (#332) 2026-06-03 14:15:33 +02:00
renovate[bot] 3ef6dd5ba5 chore(deps): update all non-major dependencies (#333) 2026-06-03 13:55:28 +02:00
renovate[bot] d57f57f3a0 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 44decbf (#328) 2026-05-14 20:21:59 +02:00
renovate[bot] c3cfaf18f5 chore(deps): update all non-major dependencies (#329) 2026-05-14 20:10:16 +02:00
Bingshen Wang 0128eed63d docs: fix CORS allowed_hosts example 2026-05-14 20:09:47 +02:00
renovate[bot] f051809a5f chore(deps): update golang.org/x/crypto/x509roots/fallback digest to fd0b90d 2026-05-05 07:42:30 +02:00
renovate[bot] f13c884b75 chore(deps): update module go.uber.org/zap to v1.28.0 2026-05-05 07:42:16 +02:00
renovate[bot] cef2e3b673 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to b9e5359 2026-04-28 07:20:05 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4fe4703e02 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to a8e9237 (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 07:53:04 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 882c2200eb chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 03ca0dc (#317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 07:42:03 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4f4045dbca chore(deps): update all non-major dependencies (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 07:40:06 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 20606cd795 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 8400f4a (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 08:47:47 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> c9ca42d89a chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 81c6cb3 (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 07:12:15 +01:00
Henrique Dias 8217bc0816 refactor: use http.Server 2026-03-14 07:46:25 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 98f8f93ebb chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 982eaa6 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:42:38 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 66049d9c08 chore(deps): update all non-major dependencies (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:40:45 +01:00
Henrique Dias 0406449b05 chore: add additional test 2026-03-09 11:25:59 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d7745cff34 chore(deps): update docker/setup-qemu-action action to v4 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 07:40:17 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d8558a77ff chore(deps): update docker/build-push-action action to v7 (#305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:39:47 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> ab5cf175ad chore(deps): update docker/login-action action to v4 (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:39:41 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f225b0418f chore(deps): update docker/metadata-action action to v6 (#307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:39:35 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d9600ee82d chore(deps): update docker/setup-buildx-action action to v4 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:39:27 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 6359115c2d chore(deps): update module golang.org/x/net to v0.51.0 (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 11:09:54 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8574b461a9 chore(deps): update goreleaser/goreleaser-action action to v7 (#302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 18:13:54 +01:00
Henrique Dias 1196ad506d fix: revert Dockerfile changes 2026-02-16 17:13:56 +01:00
Henrique Dias 4ac9a1041c fix: remove broken targets 2026-02-15 14:16:59 +01:00
Henrique Dias 27aad9d403 fix: test 2026-02-15 14:05:07 +01:00
Henrique Dias e06f26b095 feat: update default CORS settings 2026-02-15 14:02:31 +01:00
Legendary4226 04070a8ae8 docs: add compose.yml and clarifications 2026-02-15 13:57:49 +01:00
renovate[bot] bd0667aac1 chore(deps): update all non-major dependencies 2026-02-15 13:57:03 +01:00
renovate[bot] b620cd5c88 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to a408498 2026-02-15 13:54:43 +01:00
renovate[bot] c4c58fe420 chore(deps): update module github.com/coreos/go-systemd/v22 to v22.7.0 2026-02-01 13:41:40 +01:00
renovate[bot] 0ff71ca067 chore(deps): update module github.com/studio-b12/gowebdav to v0.12.0 2026-01-24 14:31:55 +01:00
renovate[bot] efdaa784b9 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 7d0074c 2026-01-18 08:34:19 +01:00
renovate[bot] 7662d629e3 chore(deps): update all non-major dependencies 2026-01-17 22:45:01 +01:00
renovate[bot] 010ca576fb chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 7dacc38 2025-12-13 09:10:00 +01:00
renovate[bot] 7b8aa1c422 chore(deps): update all non-major dependencies 2025-12-13 09:05:43 +01:00
Henrique Dias 2f98af7d1e ci: run renovate on weekends 2025-12-05 15:10:32 +01:00
renovate[bot] 50e1828b60 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 3a1c6b4 2025-12-05 14:34:40 +01:00
renovate[bot] e78d59e374 chore(deps): update module github.com/spf13/cobra to v1.10.2 2025-12-05 14:31:30 +01:00
renovate[bot] de0a102ed8 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to f4602e4 2025-12-03 08:46:31 +01:00
Henrique Dias bdfa98d00d feat: use golang.org/x/crypto/x509roots/fallback 2025-11-20 19:12:12 +01:00
renovate[bot] f0e7e23ea8 chore(deps): update actions/checkout action to v6 2025-11-20 18:55:14 +01:00
renovate[bot] 065991a195 chore(deps): update all non-major dependencies 2025-11-20 08:09:56 +01:00
Henrique Dias bb85514c8f chore: use 'chore' in all commits from renovate 2025-11-13 14:11:51 +01:00
renovate[bot] c0af549a1f fix(deps): update all non-major dependencies 2025-11-12 07:19:53 +01:00
Emmanuel Ferdman 685d89162a chore: update GoReleaser configurations
Signed-off-by: Emmanuel Ferdman <[email protected]>
2025-11-10 17:17:48 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a5496c9516 chore(deps): update golangci/golangci-lint-action action to v9 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 07:36:01 +01:00
Michael Bruns 24abd52e23 feat: enhanced logging 2025-10-30 07:55:50 +01:00
Henrique Dias 1af4c723f4 ci: fix releaser go version 2025-10-17 17:21:51 +02:00
Henrique Dias 2406836223 fix: additive permissions 2025-10-17 17:12:11 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7ca822bcaf fix(deps): update all non-major dependencies (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 15:46:19 +02:00
Chris a98d0c2616 docs: add Caddy configuration example 2025-09-11 09:33:54 +02:00
renovate[bot] 98e92d0c13 fix(deps): update module golang.org/x/net to v0.44.0 2025-09-10 14:27:35 +02:00
renovate[bot] 53416e57da fix(deps): update all non-major dependencies 2025-09-09 08:16:16 +02:00
renovate[bot] ff6637f396 chore(deps): update actions/setup-go action to v6 2025-09-06 08:32:02 +02:00
renovate[bot] 15fbcd391b fix(deps): update module github.com/studio-b12/gowebdav to v0.11.0 2025-09-06 08:31:20 +02:00
Henrique Dias 70b3630d08 chore: update dependencies 2025-09-03 07:45:22 +02:00
renovate[bot] aa014b092b fix(deps): update module github.com/spf13/pflag to v1.0.8 2025-09-03 07:43:32 +02:00
renovate[bot] b4553710eb fix(deps): update module github.com/stretchr/testify to v1.11.1 2025-08-27 20:43:15 +02:00
renovate[bot] e07e55d97a fix(deps): update module github.com/stretchr/testify to v1.11.0 2025-08-25 08:36:54 +02:00
renovate[bot] cfee0f9f0b fix(deps): update module github.com/coreos/go-systemd/v22 to v22.6.0 2025-08-21 09:46:43 +02:00
Henrique Dias 79eb02aba4 chore: temporarily disable risc docker 2025-08-15 09:35:51 +02:00
Henrique Dias 7bbe36e4c1 chore: update alpine version 2025-08-15 09:29:28 +02:00
Henrique Dias 520b7df04d chore: update dependencies 2025-08-15 09:19:31 +02:00
renovate[bot] 8f3a3298a8 chore(deps): update dependency go to 1.25.x 2025-08-15 09:15:27 +02:00
renovate[bot] 1753aa2945 chore(deps): update actions/checkout action to v5 2025-08-11 16:28:32 +02:00
renovate[bot] 4645f6f46f fix(deps): update all non-major dependencies 2025-08-08 07:36:35 +02:00
renovate[bot] e998e2a838 fix(deps): update module github.com/spf13/pflag to v1.0.7 2025-07-17 06:49:00 +02:00
renovate[bot] 3b3dedc4eb fix(deps): update module github.com/go-viper/mapstructure/v2 to v2.4.0 2025-07-15 19:23:12 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8706802dac fix(deps): update all non-major dependencies (#248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 07:47:38 +02:00
Henrique Dias 5676a1c382 chore: update renovate.json 2025-06-21 09:54:30 +02:00
Henrique Dias c938367dc0 chore: add go.mod tidy to renovate.json 2025-06-20 07:56:44 +02:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Henrique Dias
758410ba64 fix(deps): update all dependencies
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <[email protected]>
2025-06-20 07:56:09 +02:00
Henrique Dias 1bbf52e475 chore: update renovate.json 2025-06-20 07:47:58 +02:00
Henrique Dias 2c2a325215 chore: update renovate.json 2025-06-19 23:22:56 +02:00
renovate[bot] f7e006b449 fix(deps): update module github.com/go-viper/mapstructure/v2 to v2.3.0 2025-06-19 23:18:12 +02:00
renovate[bot] 49f61eab79 fix(deps): update module github.com/studio-b12/gowebdav to v0.10.0 2025-06-19 23:17:57 +02:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5d1a68c4f2 chore: add renovate.json (#235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 22:56:45 +02:00
Henrique Dias 04cee682fb docs: update Homebrew title 2025-05-25 07:45:53 +02:00
Henrique Dias 19d128cfbd docs: add brew 2025-05-25 07:44:09 +02:00
酸柠檬猹/SourLemonJuice 118a0dda88 feat: add docker build target of linux/riscv64 to workflow 2025-05-21 15:42:50 +02:00
Henrique Dias aad8e563a7 feat: add executable's directory as config path 2025-05-20 18:09:52 +02:00
Henrique Dias 8446f9bdae docs: add nginx note about copy/move 2025-05-20 18:06:26 +02:00
Henrique Dias 5226853fca fix: lock files across different users
Fixes #66.
2025-05-17 10:53:07 +02:00
Henrique Dias e350dc989a docs(readme): make it clear that path.rule is relative to user's directory 2025-05-17 10:47:57 +02:00
Henrique Dias 65de8e8578 chore: update dependencies 2025-05-17 09:14:37 +02:00
Henrique Dias f0b5a39d80 ci: go 1.24 2025-03-18 07:53:28 +01:00
Henrique Dias dbd708a76e chore: update dependencies 2025-03-18 07:53:28 +01:00
Henrique Dias f61b7cab3b docs: cleanup comments 2025-02-12 07:42:15 +01:00
Henrique Dias 3cfaa0da3e fix: return correct amount of written bytes 2025-02-12 07:41:21 +01:00
networkException f7b78cd834 fix: support HEAD method on collections 2025-02-12 07:41:09 +01:00
Henrique Dias 4b4e555ed5 fix: bring back the prefix test 2025-01-16 07:10:07 +01:00
Henrique Dias 6aeb3f8a30 fix: refactoring previous fix in attempt to address ongoing issues
See #211 and #215
2025-01-15 19:35:03 +01:00
Henrique Dias 59c5f1343f Revert "fix: rename/copy when using prefix"
This reverts commit 617496c019.
2025-01-15 19:18:40 +01:00
Henrique Dias 617496c019 fix: rename/copy when using prefix 2025-01-04 18:45:20 +01:00
Andrey Denisov 3790d7de08 docs(readme): improve fail2ban description 2024-12-30 20:10:07 +01:00
Henrique Dias eaf42b03e9 feat: add bcrypt command 2024-12-22 10:28:49 +01:00
Henrique Dias dce6010b61 chore: update dependencies 2024-12-21 08:04:50 +01:00
Henrique Dias 88863d7242 ci: update golangci-lint 2024-12-15 18:30:50 +01:00
Henrique Dias 2c96db92aa chore: update dependencies 2024-12-15 18:30:28 +01:00
networkException 79bc17afab feat: add support for systemd listener activation sockets
To support starting a network service on demand and to
support a "least privilege-approach" with regards to the
permission a network service process needs to have, systemd
supports opening a network socket on behalf of the service
and passing it as an open file descriptor.

The service gets notified about open file descriptors for
this purpose as well as metadata such as named listeners
via environment variables.

This patch adds support for prefixing the listen address
passed with --address with "sd-listen-fd:" to access these
file descriptors, taking either a listener name passed using
the `LISTEN_FDNAMES` environment variable or `LISTEN_FD_$n`
for unnamed file descriptiors where `n` is the id of the
descriptor starting at 3 (LISTEN_FD_3).

See sd_listen_fds(3)
2024-12-11 17:29:03 +01:00
Henrique Dias 51b101d3d8 feat: rules behavior 2024-12-07 12:07:12 +01:00
Henrique Dias ca7f3374d5 chore: update dependencies 2024-11-28 16:59:04 +01:00
Henrique Dias 64bbdc7b15 fix: error if rule has no regex or path 2024-11-28 16:57:10 +01:00
24 changed files with 747 additions and 208 deletions
+3 -3
View File
@@ -12,10 +12,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version: "1.23.x"
go-version: "1.26.x"
- run: go build .
env:
CGO_ENABLED: '0'
+7 -7
View File
@@ -18,22 +18,22 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -41,7 +41,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
hacdias/webdav
@@ -55,7 +55,7 @@ jobs:
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
+5 -5
View File
@@ -12,10 +12,10 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version: "1.23.x"
- uses: golangci/golangci-lint-action@v6
go-version: "1.26.x"
- uses: golangci/golangci-lint-action@v9
with:
version: "v1.60"
version: "latest"
+5 -3
View File
@@ -12,11 +12,13 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: goreleaser/goreleaser-action@v6
- uses: actions/setup-go@v6
with:
go-version: "1.26.x"
- uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
+3 -3
View File
@@ -13,9 +13,9 @@ jobs:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version: "1.23.x"
go-version: "1.26.x"
- name: Run test with coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
+1 -1
View File
@@ -1,2 +1,2 @@
dist/
webdav
webdav
+7 -3
View File
@@ -38,16 +38,20 @@ builds:
- goos: openbsd
goarch: arm
goarm: 6
# Broken as of Go 1.24, deprecated as of Go 1.26
- goos: freebsd
goarch: arm
goarm: 6
- goos: windows
goarch: arm
archives:
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
format: tar.gz
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
release:
github:
+1 -4
View File
@@ -1,9 +1,7 @@
FROM golang:1.23-alpine3.20 AS build
FROM golang:1.26-alpine3.22 AS build
ARG VERSION="untracked"
RUN apk --update add ca-certificates
WORKDIR /webdav/
COPY ./go.mod ./
@@ -15,7 +13,6 @@ RUN go build -o main -trimpath -ldflags="-s -w -X 'github.com/hacdias/webdav/v5/
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /webdav/main /bin/webdav
EXPOSE 6065
+126 -42
View File
@@ -14,6 +14,14 @@ For a manual install, please refer to the [releases](https://github.com/hacdias/
go install github.com/hacdias/webdav/v5@latest
```
### Homebrew
If you have Homebrew available on your system, you can also install `webdav` using it:
```
brew install webdav
```
### Docker
Docker images are provided on both [GitHub's registry](https://github.com/hacdias/webdav/pkgs/container/webdav) and [Docker Hub](https://hub.docker.com/r/hacdias/webdav). You can pull the images using one of the following two commands. Note that this commands pull the latest released version. You can use specific tags to pin specific versions, or use `main` for the development branch.
@@ -30,28 +38,32 @@ docker pull hacdias/webdav:latest
For usage information regarding the CLI, run `webdav --help`.
### Docker
### Container
To use with Docker, you need to provide a configuration file and mount the data directories. For example, let's take the following configuration file that simply sets the port to `6060` and the directory to `/data`.
To run the container, you can refer to the `compose.yml` file which provides a minimal setup. Additionally, you also need to create a configuration file, as [explained below](#configuration).
```yaml
port: 6060
directory: /data
```
You can now run with the following Docker command, where you mount the configuration file inside the container, and the data directory too, as well as forwarding the port 6060. You will need to change this to match your own configuration.
The equivalent Docker command to the aforementioned compose file would be as follows:
```bash
docker run \
-p 6060:6060 \
-v $(pwd)/config.yml:/config.yml:ro \
-v $(pwd)/data:/data \
-p 6065:6065 \
-v ./config.yml:/config.yml:ro \
-v ./data:/data \
ghcr.io/hacdias/webdav -c /config.yml
```
If you are using [fail2ban](#fail2ban-setup), it would be helpful to add the parameters listed below. They will assist in analyzing
the log.
```bash
--log-driver journald \
--name webdav \
```
## Configuration
The configuration can be provided as a YAML, JSON or TOML file. Below is an example of a YAML configuration file with all the options available, as well as what they mean.
The configuration can be provided as a YAML, JSON or TOML file. Below is an example of a YAML configuration file with
all the options available, as well as what they mean.
```yaml
address: 0.0.0.0
@@ -78,17 +90,31 @@ behindProxy: false
# The directory that will be able to be accessed by the users when connecting.
# This directory will be used by users unless they have their own 'directory' defined.
# Default is '.' (current directory).
directory: .
# By default it points to the working directory. In the case of the compose file above,
# that is /data.
directory: /data
# The default permissions for users. This is a case insensitive option. Possible
# permissions: C (Create), R (Read), U (Update), D (Delete). You can combine multiple
# permissions. For example, to allow to read and create, set "RC". Default is "R".
permissions: R
# The default permissions rules for users. Default is none.
# The default permissions rules for users. Default is none. Rules are applied
# from last to first, that is, the first rule that matches the request, starting
# from the end, will be applied to the request. Rule paths are always relative to
# the user's directory.
rules: []
# The behavior of redefining the rules for users. It can be:
# - overwrite: when a user has rules defined, these will overwrite any global
# rules already defined. That is, the global rules are not applicable to the
# user.
# - append: when a user has rules defined, these will be appended to the global
# rules already defined. That is, for this user, their own specific rules will
# be checked first, and then the global rules.
# Default is 'overwrite'.
rulesBehavior: overwrite
# Logging configuration
log:
# Logging format ('console', 'json'). Default is 'console'.
@@ -97,57 +123,86 @@ log:
colors: true
# Logging outputs. You can have more than one output. Default is only 'stderr'.
outputs:
- stderr
- stderr
# CORS configuration
cors:
# Whether or not CORS configuration should be applied. Default is 'false'.
enabled: true
credentials: true
allowed_headers:
- Depth
# The following are the default CORS settings when it is enabled.
allowed_hosts:
- http://localhost:8080
- '*'
allowed_headers:
- Authorization
- Content-Type
- Depth
- Destination
- If
- Lock-Token
- Overwrite
- TimeOut
- Translate
allowed_methods:
- COPY
- DELETE
- GET
exposed_headers:
- Content-Length
- Content-Range
- HEAD
- LOCK
- UNLOCK
- MKCOL
- MOVE
- OPTIONS
- POST
- PROPFIND
- PROPPATCH
- PUT
exposed_headers: []
# The list of users. If the list is empty, then there will be no authentication.
# Otherwise, basic authentication will automatically be configured.
#
# If you're delegating the authentication to a different service, you can proxy
# the username using basic authentication, and then disable webdav's password
# check using the option:
#
# noPassword: true
# You define here the list of users.
# Basic authentication is automatically be configured when users are detected
# below, else there will be no authentication.
# Customize to your needs and don't forget to comment out the users you don't need.
users:
# Example 'admin' user with plaintext password.
- username: admin
password: admin
# Example 'john' user with bcrypt encrypted password, with custom directory.
# Tip: you can generate a bcrypt-encrypted password by using the 'webdav bcrypt'
# command lint utility, or htpasswd on Linux.
- username: john
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
directory: /another/path
directory: /data/john
# Example user whose details will be picked up from the environment.
- username: "{env}ENV_USERNAME"
password: "{env}ENV_PASSWORD"
# Example user with advanced control over his permissions
- username: basic
password: basic
# Override default permissions.
permissions: CRUD
permissions: CRUD # Override default permissions.
rules:
# With this rule, the user CANNOT access /some/files.
# With this rule, the user CANNOT access {user directory}/some/files.
- path: /some/file
permissions: none
# With this rule, the user CAN create, read, update and delete within /public/access.
# With this rule, the user CAN create, read, update and delete within
# {user directory}/public/access.
- path: /public/access/
permissions: CRUD
# With this rule, the user CAN read and update all files ending with .js. It uses
# a regular expression.
# With this rule, the user CAN read and update all files ending with .js.
# It uses a regular expression.
- regex: "^.+.js$"
permissions: RU
# Example user for android SeedVault backuping
- username: android
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
directory: /data/android
permissions: CRUD
# If you're delegating the authentication to a different service, you can proxy
# the username using basic authentication, and then disable webdav's password
# check using the option:
# noPassword: true
```
### CORS
@@ -161,7 +216,9 @@ The `allowed_*` properties are optional, the default value for each of them will
### Reverse Proxy Service
When using a reverse proxy implementation, like Caddy, Nginx, or Apache, note that you need to forward the correct headers in order to avoid 502 errors. Here's a Nginx configuration example:
When using a reverse proxy implementation, like Caddy, Nginx, or Apache, note that you need to forward the correct headers in order to avoid 502 errors.
#### Nginx Configuration Example
```nginx
location / {
@@ -171,6 +228,33 @@ location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
# Ensure COPY and MOVE commands work. Change https://example.com to the
# correct address where the WebDAV server will be deployed at.
set $dest $http_destination;
if ($http_destination ~ "^https://example.com(?<path>(.+))") {
set $dest /$path;
}
proxy_set_header Destination $dest;
}
```
#### Caddy Configuration Example
```Caddyfile
example.com {
tls internal # for local development
# tls [email protected] # so that Caddy gets certs for you via Letsencrypt
# Rewrites destination to remove host and include only the path e.g. /test.txt
@hasDest header_regexp dest ^https?://[^/]+(.*)$
header @hasDest Destination {re.dest.1}
# if running on the same network in docker you can just point to the service name e.g. webdav:6065
reverse_proxy 127.0.0.1:6065 {
header_up X-Real-IP {remote_host}
header_up REMOTE-HOST {remote_host}
}
}
```
@@ -209,10 +293,8 @@ before = common.conf
[Definition]
# Failregex to match "invalid password" and extract remote_address only
failregex = ^.*invalid password\s*\{.*"remote_address":\s*"<HOST>"\s*\}
# Failregex to match "invalid username" and extract remote_address only (if applicable)
failregex += ^.*invalid username\s*\{.*"remote_address":\s*"<HOST>"\s*\}
failregex = ^.*invalid password\s*\{.*"remote_address":\s*"<HOST>:\d+"\s*\}
^.*invalid username\s*\{.*"remote_address":\s*"<HOST>:\d+"\s*\}
ignoreregex =
```
@@ -237,6 +319,8 @@ ignoreself = false
- Replace `[your_port]` with the port your WebDAV server is running on.
- Replace `[your_log_path]` with the path to your WebDAV log file.
If you use it with Docker and `--log-driver journald`, replace `logpath` with `journalmatch = CONTAINER_NAME=[your_container_name]`
#### Final Steps
1. Restart Fail2Ban to apply these configurations:
+49
View File
@@ -0,0 +1,49 @@
package cmd
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
func init() {
flags := bcryptCmd.Flags()
flags.IntP("cost", "c", bcrypt.DefaultCost, "cost used to generate password, higher cost leads to slower verification times")
rootCmd.AddCommand(bcryptCmd)
}
var bcryptCmd = &cobra.Command{
Use: "bcrypt",
Short: "Generate a bcrypt encrypted password",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cost, err := cmd.Flags().GetInt("cost")
if err != nil {
return err
}
if cost < bcrypt.MinCost {
return fmt.Errorf("given cost cannot be under minimum cost of %d", bcrypt.MinCost)
}
if cost > bcrypt.MaxCost {
return fmt.Errorf("given cost cannot be over maximum cost of %d", bcrypt.MaxCost)
}
pwd := args[0]
if pwd == "" {
return errors.New("password argument must not be empty")
}
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
if err != nil {
return err
}
fmt.Println(string(hash))
return nil
},
}
+22 -4
View File
@@ -1,6 +1,7 @@
package cmd
import (
"context"
"errors"
"fmt"
"net"
@@ -10,6 +11,7 @@ import (
"strings"
"syscall"
"github.com/coreos/go-systemd/v22/activation"
"github.com/hacdias/webdav/v5/lib"
"github.com/spf13/cobra"
"go.uber.org/zap"
@@ -79,6 +81,8 @@ set WD_CERT.`,
return err
}
server := &http.Server{Handler: handler}
// Trap exiting signals
quit := make(chan os.Signal, 1)
@@ -87,9 +91,9 @@ set WD_CERT.`,
var err error
if cfg.TLS {
err = http.ServeTLS(listener, handler, cfg.Cert, cfg.Key)
err = server.ServeTLS(listener, cfg.Cert, cfg.Key)
} else {
err = http.Serve(listener, handler)
err = server.Serve(listener)
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
@@ -103,7 +107,7 @@ set WD_CERT.`,
signal := <-quit
zap.L().Info("caught signal, shutting down", zap.Stringer("signal", signal))
_ = listener.Close()
_ = server.Shutdown(context.Background())
return nil
},
@@ -115,7 +119,21 @@ func getListener(cfg *lib.Config) (net.Listener, error) {
network string
)
if strings.HasPrefix(cfg.Address, "unix:") {
if strings.HasPrefix(cfg.Address, "sd-listen-fd:") {
listeners, err := activation.ListenersWithNames()
if err != nil {
return nil, err
}
address := cfg.Address[13:]
listener, ok := listeners[address]
if !ok || len(listener) < 1 {
return nil, errors.New("unknown sd-listen-fd address '" + address + "'")
}
return listener[0], nil
} else if strings.HasPrefix(cfg.Address, "unix:") {
address = cfg.Address[5:]
network = "unix"
} else {
+14
View File
@@ -0,0 +1,14 @@
name: webdav
services:
app:
container_name: webdav
image: 'webdav:test'
ports:
- '6065:6065'
security_opt:
- label=type:container_runtime_t
restart: always
volumes:
- ./data:/data
- ./config.yml:/config.yml:ro
+21 -24
View File
@@ -1,39 +1,36 @@
module github.com/hacdias/webdav/v5
go 1.23
go 1.25.0
require (
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/coreos/go-systemd/v22 v22.7.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/rs/cors v1.11.1
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/studio-b12/gowebdav v0.9.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/studio-b12/gowebdav v0.12.0
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.53.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260618162219-0b316e7ee409
golang.org/x/net v0.56.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+42 -48
View File
@@ -1,29 +1,24 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -31,48 +26,47 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260618162219-0b316e7ee409 h1:tCCOYc6P3pGx8nU+VbcOGw5ed3jKRWNt2Vrk7MY4ugw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260618162219-0b316e7ee409/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+24 -7
View File
@@ -3,6 +3,7 @@ package lib
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
@@ -40,7 +41,7 @@ type Config struct {
}
func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
v := viper.New()
v := viper.NewWithOptions(viper.ExperimentalBindStruct())
// Configure flags bindings
if flags != nil {
@@ -53,6 +54,10 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
// Configuration file settings
v.AddConfigPath(".")
v.AddConfigPath("/etc/webdav/")
if path, err := os.Executable(); err == nil {
v.AddConfigPath(filepath.Dir(path))
}
v.SetConfigName("config")
if filename != "" {
v.SetConfigFile(filename)
@@ -62,9 +67,6 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
v.SetEnvPrefix("wd")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// TODO: use new env struct bind feature when it's released in viper.
// This should make it redundant to set defaults for things that are
// empty or false.
// Defaults shared with flags
v.SetDefault("TLS", DefaultTLS)
@@ -75,6 +77,7 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
v.SetDefault("Prefix", DefaultPrefix)
// Other defaults
v.SetDefault("RulesBehavior", RulesOverwrite)
v.SetDefault("Directory", ".")
v.SetDefault("Permissions", "R")
v.SetDefault("Debug", false)
@@ -83,9 +86,9 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
v.SetDefault("Log.Format", "console")
v.SetDefault("Log.Outputs", []string{"stderr"})
v.SetDefault("Log.Colors", true)
v.SetDefault("CORS.Allowed_Headers", []string{"*"})
v.SetDefault("CORS.Allowed_Hosts", []string{"*"})
v.SetDefault("CORS.Allowed_Methods", []string{"*"})
v.SetDefault("CORS.Allowed_Headers", []string{"Authorization", "Content-Type", "Depth", "Destination", "If", "Lock-Token", "Overwrite"})
v.SetDefault("CORS.Allowed_Methods", []string{"COPY", "DELETE", "GET", "HEAD", "LOCK", "MKCOL", "MOVE", "OPTIONS", "POST", "PROPFIND", "PROPPATCH", "PUT", "UNLOCK"})
// Read and unmarshal configuration
err := v.ReadInConfig()
@@ -115,7 +118,21 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
cfg.Users[i].Permissions = cfg.Permissions
}
if !v.IsSet(fmt.Sprintf("Users.%d.Rules", i)) {
if !v.IsSet(fmt.Sprintf("Users.%d.RulesBehavior", i)) {
cfg.Users[i].RulesBehavior = cfg.RulesBehavior
}
if v.IsSet(fmt.Sprintf("Users.%d.Rules", i)) {
switch cfg.Users[i].RulesBehavior {
case RulesOverwrite:
// Do nothing
case RulesAppend:
rules := append([]*Rule{}, cfg.Rules...)
rules = append(rules, cfg.Users[i].Rules...)
cfg.Users[i].Rules = rules
}
} else {
cfg.Users[i].Rules = cfg.Rules
}
}
+99 -12
View File
@@ -51,9 +51,9 @@ func TestConfigDefaults(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dir, cfg.Directory)
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHeaders)
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHosts)
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedMethods)
require.EqualValues(t, []string{"Authorization", "Content-Type", "Depth", "Destination", "If", "Lock-Token", "Overwrite"}, cfg.CORS.AllowedHeaders)
require.EqualValues(t, []string{"COPY", "DELETE", "GET", "HEAD", "LOCK", "MKCOL", "MOVE", "OPTIONS", "POST", "PROPFIND", "PROPPATCH", "PUT", "UNLOCK"}, cfg.CORS.AllowedMethods)
}
func TestConfigCascade(t *testing.T) {
@@ -192,24 +192,111 @@ cors:
}
func TestConfigRules(t *testing.T) {
content := `
t.Run("Only Regex or Path", func(t *testing.T) {
content := `
directory: /
rules:
- regex: '^.+\.js$'
path: /public/access/`
writeAndParseConfigWithError(t, content, ".yaml", "cannot define both regex and path")
})
t.Run("Regex or Path Required", func(t *testing.T) {
content := `
directory: /
rules:
- permissions: CRUD`
writeAndParseConfigWithError(t, content, ".yaml", "must either define a path of a regex")
})
t.Run("Parse", func(t *testing.T) {
content := `
directory: /
rules:
- regex: '^.+\.js$'
- path: /public/access/`
cfg := writeAndParseConfig(t, content, ".yaml")
require.NoError(t, cfg.Validate())
cfg := writeAndParseConfig(t, content, ".yaml")
require.NoError(t, cfg.Validate())
require.Len(t, cfg.Rules, 2)
require.Len(t, cfg.Rules, 2)
require.Empty(t, cfg.Rules[0].Path)
require.NotNil(t, cfg.Rules[0].Regex)
require.True(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.js"))
require.False(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.ts"))
require.Empty(t, cfg.Rules[0].Path)
require.NotNil(t, cfg.Rules[0].Regex)
require.True(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.js"))
require.False(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.ts"))
require.NotEmpty(t, cfg.Rules[1].Path)
require.Nil(t, cfg.Rules[1].Regex)
require.NotEmpty(t, cfg.Rules[1].Path)
require.Nil(t, cfg.Rules[1].Regex)
})
t.Run("Rules Behavior (Default: Overwrite)", func(t *testing.T) {
content := `
directory: /
rules:
- regex: '^.+\.js$'
- path: /public/access/
users:
- username: foo
password: bar
rules:
- path: /private/access/`
cfg := writeAndParseConfig(t, content, ".yaml")
require.NoError(t, cfg.Validate())
require.Len(t, cfg.Rules, 2)
require.Empty(t, cfg.Rules[0].Path)
require.NotNil(t, cfg.Rules[0].Regex)
require.True(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.js"))
require.False(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.ts"))
require.EqualValues(t, "/public/access/", cfg.Rules[1].Path)
require.Nil(t, cfg.Rules[1].Regex)
require.Len(t, cfg.Users, 1)
require.Len(t, cfg.Users[0].Rules, 1)
require.EqualValues(t, "/private/access/", cfg.Users[0].Rules[0].Path)
})
t.Run("Rules Behavior (Append)", func(t *testing.T) {
content := `
directory: /
rules:
- regex: '^.+\.js$'
- path: /public/access/
rulesBehavior: append
users:
- username: foo
password: bar
rules:
- path: /private/access/`
cfg := writeAndParseConfig(t, content, ".yaml")
require.NoError(t, cfg.Validate())
require.Len(t, cfg.Rules, 2)
require.Empty(t, cfg.Rules[0].Path)
require.NotNil(t, cfg.Rules[0].Regex)
require.True(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.js"))
require.False(t, cfg.Rules[0].Regex.MatchString("/my/path/to/file.ts"))
require.EqualValues(t, "/public/access/", cfg.Rules[1].Path)
require.Nil(t, cfg.Rules[1].Regex)
require.Len(t, cfg.Users, 1)
require.Len(t, cfg.Users[0].Rules, 3)
require.EqualValues(t, cfg.Rules[0], cfg.Users[0].Rules[0])
require.EqualValues(t, cfg.Rules[1], cfg.Users[0].Rules[1])
require.EqualValues(t, "/private/access/", cfg.Users[0].Rules[2].Path)
})
}
func TestConfigEnv(t *testing.T) {
+1 -1
View File
@@ -47,7 +47,7 @@ type noSniffFileInfo struct {
}
func (w noSniffFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
if mimeType := mime.TypeByExtension(path.Ext(w.FileInfo.Name())); mimeType != "" {
if mimeType := mime.TypeByExtension(path.Ext(w.Name())); mimeType != "" {
// We can figure out the mime from the extension.
return mimeType, nil
} else {
+45 -25
View File
@@ -2,7 +2,6 @@ package lib
import (
"net/http"
"net/url"
"os"
"strings"
@@ -24,6 +23,13 @@ type Handler struct {
}
func NewHandler(c *Config) (http.Handler, error) {
ls := webdav.NewMemLS()
logFunc := func(r *http.Request, err error) {
lZap := getRequestLogger(r, c.BehindProxy)
lZap.Debug("handle webdav request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Error(err))
}
h := &Handler{
noPassword: c.NoPassword,
behindProxy: c.BehindProxy,
@@ -37,7 +43,11 @@ func NewHandler(c *Config) (http.Handler, error) {
Dir: webdav.Dir(c.Directory),
noSniff: c.NoSniff,
},
LockSystem: webdav.NewMemLS(),
LockSystem: &lockSystem{
LockSystem: ls,
directory: c.Directory,
},
Logger: logFunc,
},
},
users: map[string]*handlerUser{},
@@ -52,7 +62,11 @@ func NewHandler(c *Config) (http.Handler, error) {
Dir: webdav.Dir(u.Directory),
noSniff: c.NoSniff,
},
LockSystem: webdav.NewMemLS(),
LockSystem: &lockSystem{
LockSystem: ls,
directory: u.Directory,
},
Logger: logFunc,
},
}
}
@@ -83,13 +97,12 @@ func NewHandler(c *Config) (http.Handler, error) {
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user := h.user
lZap := getRequestLogger(r, h.behindProxy)
// Authentication
if len(h.users) > 0 {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
// Retrieve the real client IP address using the updated helper function
remoteAddr := getRealRemoteIP(r, h.behindProxy)
// Gets the correct user for this request.
username, password, ok := r.BasicAuth()
if !ok {
@@ -100,42 +113,37 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, ok = h.users[username]
if !ok {
// Log invalid username
zap.L().Info("invalid username", zap.String("username", username), zap.String("remote_address", remoteAddr))
lZap.Info("invalid username", zap.String("username", username))
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
if !h.noPassword && !user.checkPassword(password) {
// Log invalid password
zap.L().Info("invalid password", zap.String("username", username), zap.String("remote_address", remoteAddr))
lZap.Info("invalid password", zap.String("username", username))
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
// Log successful authorization
zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr))
lZap.Info("user authorized", zap.String("username", username))
}
// Cleanup destination header if it's present by stripping out the prefix
// and only keeping the path.
if destination := r.Header.Get("Destination"); destination != "" {
u, err := url.Parse(destination)
if err == nil {
destination = strings.TrimPrefix(u.Path, user.Prefix)
if !strings.HasPrefix(destination, "/") {
destination = "/" + destination
}
r.Header.Set("Destination", destination)
}
// Convert the HTTP request into an internal request type
req, err := newRequest(r, h.user.Prefix)
if err != nil {
lZap.Info("invalid request path or destination", zap.Error(err))
http.Error(w, "Invalid request path or destination", http.StatusBadRequest)
return
}
// Checks for user permissions relatively to this PATH.
allowed := user.Allowed(r, func(filename string) bool {
allowed := user.Allowed(req, func(filename string) bool {
_, err := user.FileSystem.Stat(r.Context(), filename)
return !os.IsNotExist(err)
})
zap.L().Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))
lZap.Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))
if !allowed {
w.WriteHeader(http.StatusForbidden)
@@ -152,8 +160,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// "index.html" resource, a human-readable view of the contents of
// the collection, or something else altogether.
//
// Get, when applied to collection, will return the same as PROPFIND method.
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, user.Prefix) {
// Similarly, since the definition of HEAD is a GET without a response
// message body, the semantics of HEAD are unmodified when applied to
// collection resources.
//
// GET (or HEAD), when applied to collection, will return the same as PROPFIND method.
if (r.Method == "GET" || r.Method == "HEAD") && strings.HasPrefix(r.URL.Path, user.Prefix) {
info, err := user.FileSystem.Stat(r.Context(), strings.TrimPrefix(r.URL.Path, user.Prefix))
if err == nil && info.IsDir() {
r.Method = "PROPFIND"
@@ -168,6 +180,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user.ServeHTTP(w, r)
}
// getRequestLogger creates a zap.Logger using the request remote ip.
func getRequestLogger(r *http.Request, behindProxy bool) *zap.Logger {
// Retrieve the real client IP address using the updated helper function
remoteAddr := getRealRemoteIP(r, behindProxy)
return zap.L().With(zap.String("remote_address", remoteAddr))
}
// getRealRemoteIP retrieves the client's actual IP address, considering reverse proxies.
func getRealRemoteIP(r *http.Request, behindProxy bool) string {
if behindProxy {
@@ -183,5 +203,5 @@ type responseWriterNoBody struct {
}
func (w responseWriterNoBody) Write(data []byte) (int, error) {
return 0, nil
return len(data), nil
}
+128 -1
View File
@@ -217,7 +217,7 @@ users:
})
}
func TestServerRules(t *testing.T) {
func TestServerRulesRestrictive(t *testing.T) {
t.Parallel()
dir := makeTestDirectory(t, map[string][]byte{
@@ -290,6 +290,133 @@ users:
err = client.Write("/c/b.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
err = client.MkdirAll("/d/foo/bar", 0666)
require.NoError(t, err)
err = client.Write("/d/foo/bar/test.txt", []byte("test"), 0666)
require.NoError(t, err)
}
func TestServerRulesAdditive(t *testing.T) {
t.Parallel()
dir := makeTestDirectory(t, map[string][]byte{
"foo.txt": []byte("foo"),
"bar.js": []byte("foo js"),
"a/foo.js": []byte("foo js"),
"a/foo.txt": []byte("foo txt"),
"b/foo.txt": []byte("foo b"),
})
srv := makeTestServer(t, fmt.Sprintf(`
directory: %s
permissions: none
users:
- username: basic
password: basic
rules:
- regex: "^.+.js$"
permissions: R
- path: "/a/foo.txt"
permissions: CRU
- path: "/b/"
permissions: D
`, dir))
client := gowebdav.NewClient(srv.URL, "basic", "basic")
_, err := client.ReadDir("/")
require.ErrorContains(t, err, "403")
err = client.Write("/foo.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
err = client.Write("/new.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
err = client.Copy("/bar.js", "/a/foo.txt", true)
require.NoError(t, err)
err = client.Remove("/b/foo.txt")
require.NoError(t, err)
}
func TestServerRulesPrefix(t *testing.T) {
t.Parallel()
dir := makeTestDirectory(t, map[string][]byte{
"foo.txt": []byte("foo"),
"bar.js": []byte("foo js"),
"a/foo.js": []byte("foo js"),
"a/foo.txt": []byte("foo txt"),
"b/foo.txt": []byte("foo b"),
"c/a.txt": []byte("b"),
"c/b.txt": []byte("b"),
"c/c.txt": []byte("b"),
})
srv := makeTestServer(t, fmt.Sprintf(`
directory: %s
permissions: CRUD
prefix: /prefix
users:
- username: basic
password: basic
rules:
- regex: "^.+.js$"
permissions: R
- path: "/b/"
permissions: R
- path: "/a/foo.txt"
permissions: none
- path: "/c/"
permissions: none
`, dir))
client := gowebdav.NewClient(srv.URL, "basic", "basic")
files, err := client.ReadDir("/prefix")
require.NoError(t, err)
require.Len(t, files, 5)
err = client.Write("/prefix/foo.txt", []byte("new"), 0666)
require.NoError(t, err)
err = client.Write("/prefix/new.txt", []byte("new"), 0666)
require.NoError(t, err)
err = client.Copy("/prefix/bar.js", "/prefix/b/bar.js", false)
require.ErrorContains(t, err, "403")
err = client.Copy("/prefix/bar.js", "/prefix/bar.jsx", false)
require.NoError(t, err)
err = client.Copy("/prefix/b/foo.txt", "/prefix/foo1.txt", false)
require.NoError(t, err)
err = client.Rename("/prefix/b/foo.txt", "/prefix/foo2.txt", false)
require.ErrorContains(t, err, "403")
_, err = client.Read("/prefix/a/foo.txt")
require.ErrorContains(t, err, "403")
err = client.Write("/prefix/a/foo.js", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
err = client.Write("/prefix/b/foo.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
_, err = client.ReadDir("/prefix/c")
require.ErrorContains(t, err, "403")
_, err = client.Read("/prefix/c/a.txt")
require.ErrorContains(t, err, "403")
err = client.Write("/prefix/c/b.txt", []byte("new"), 0666)
require.ErrorContains(t, err, "403")
}
func TestServerPermissions(t *testing.T) {
+35
View File
@@ -0,0 +1,35 @@
package lib
import (
"path/filepath"
"time"
"golang.org/x/net/webdav"
)
var _ webdav.LockSystem = &lockSystem{}
// LockSystem wraps a [webdav.LockSystem] with a root directory, allowing
// to reuse the same [webdav.LockSystem] for multiple users with different base
// directories, meaning we can correctly lock the files across different users.
type lockSystem struct {
webdav.LockSystem
directory string
}
func (l *lockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) {
if name0 != "" {
name0 = filepath.Join(l.directory, name0)
}
if name1 != "" {
name1 = filepath.Join(l.directory, name1)
}
return l.LockSystem.Confirm(now, name0, name1, conditions...)
}
func (l *lockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
details.Root = filepath.Join(l.directory, details.Root)
return l.LockSystem.Create(now, details)
}
+35 -15
View File
@@ -3,7 +3,6 @@ package lib
import (
"errors"
"fmt"
"net/http"
"path/filepath"
"regexp"
"strings"
@@ -16,6 +15,10 @@ type Rule struct {
}
func (r *Rule) Validate() error {
if r.Regex == nil && r.Path == "" {
return errors.New("invalid rule: must either define a path of a regex")
}
if r.Regex != nil && r.Path != "" {
return errors.New("invalid rule: cannot define both regex and path")
}
@@ -32,22 +35,32 @@ func (r *Rule) Matches(path string) bool {
return strings.HasPrefix(path, r.Path)
}
type RulesBehavior string
const (
RulesOverwrite RulesBehavior = "overwrite"
RulesAppend RulesBehavior = "append"
)
type UserPermissions struct {
Directory string
Permissions Permissions
Rules []*Rule
Directory string
Permissions Permissions
Rules []*Rule
RulesBehavior RulesBehavior
}
// Allowed checks if the user has permission to access a directory/file
func (p UserPermissions) Allowed(r *http.Request, fileExists func(string) bool) bool {
func (p UserPermissions) Allowed(r *request, fileExists func(string) bool) bool {
// For COPY and MOVE requests, we first check the permissions for the destination
// path. As soon as a rule matches and does not allow the operation at the destination,
// we fail immediately. If no rule matches, we check the global permissions.
if r.Method == "COPY" || r.Method == "MOVE" {
dst := r.Header.Get("Destination")
if r.method == "COPY" || r.method == "MOVE" {
dst := r.destination
ruleMatched := false
for i := len(p.Rules) - 1; i >= 0; i-- {
if p.Rules[i].Matches(dst) {
ruleMatched = true
if !p.Rules[i].Permissions.AllowedDestination(r, fileExists) {
return false
}
@@ -57,7 +70,7 @@ func (p UserPermissions) Allowed(r *http.Request, fileExists func(string) bool)
}
}
if !p.Permissions.AllowedDestination(r, fileExists) {
if !ruleMatched && !p.Permissions.AllowedDestination(r, fileExists) {
return false
}
}
@@ -65,7 +78,7 @@ func (p UserPermissions) Allowed(r *http.Request, fileExists func(string) bool)
// Go through rules beginning from the last one, and check the permissions at
// the source. The first matched rule returns.
for i := len(p.Rules) - 1; i >= 0; i-- {
if p.Rules[i].Matches(r.URL.Path) {
if p.Rules[i].Matches(r.path) {
return p.Rules[i].Permissions.Allowed(r, fileExists)
}
}
@@ -87,6 +100,13 @@ func (p *UserPermissions) Validate() error {
}
}
switch p.RulesBehavior {
case RulesAppend, RulesOverwrite:
// Good to go
default:
return fmt.Errorf("invalid rule behavior: %s", p.RulesBehavior)
}
return nil
}
@@ -123,8 +143,8 @@ func (p *Permissions) UnmarshalText(data []byte) error {
// Allowed returns whether this permission set has permissions to execute this
// request in the source directory. This applies to all requests with all methods.
func (p Permissions) Allowed(r *http.Request, fileExists func(string) bool) bool {
switch r.Method {
func (p Permissions) Allowed(r *request, fileExists func(string) bool) bool {
switch r.method {
case "GET", "HEAD", "OPTIONS", "POST", "PROPFIND":
// Note: POST backend implementation just returns the same thing as GET.
return p.Read
@@ -133,7 +153,7 @@ func (p Permissions) Allowed(r *http.Request, fileExists func(string) bool) bool
case "PROPPATCH":
return p.Update
case "PUT":
if fileExists(r.URL.Path) {
if fileExists(r.path) {
return p.Update
} else {
return p.Create
@@ -153,10 +173,10 @@ func (p Permissions) Allowed(r *http.Request, fileExists func(string) bool) bool
// AllowedDestination returns whether this permissions set has permissions to execute this
// request in the destination directory. This only applies for COPY and MOVE requests.
func (p Permissions) AllowedDestination(r *http.Request, fileExists func(string) bool) bool {
switch r.Method {
func (p Permissions) AllowedDestination(r *request, fileExists func(string) bool) bool {
switch r.method {
case "COPY", "MOVE":
if fileExists(r.Header.Get("Destination")) {
if fileExists(r.destination) {
return p.Update
} else {
return p.Create
+57
View File
@@ -0,0 +1,57 @@
package lib
import (
"errors"
"net/http"
"net/url"
"strings"
)
type request struct {
method string
path string
destination string
}
func newRequest(r *http.Request, prefix string) (*request, error) {
ctx := &request{
method: r.Method,
}
if destination := r.Header.Get("Destination"); destination != "" {
u, err := url.Parse(destination)
if err != nil {
return nil, errors.New("invalid destination header")
}
if prefix != "" {
destination = strings.TrimPrefix(u.Path, prefix)
if len(destination) >= len(u.Path) {
return nil, errors.New("invalid url prefix")
}
}
if !strings.HasPrefix(destination, "/") {
destination = "/" + destination
}
ctx.destination = destination
}
path := r.URL.Path
if prefix != "" {
path = strings.TrimPrefix(r.URL.Path, prefix)
if len(path) >= len(r.URL.Path) {
return nil, errors.New("invalid url prefix")
}
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
ctx.path = path
return ctx, nil
}
+2
View File
@@ -2,6 +2,8 @@ package main
import (
"github.com/hacdias/webdav/v5/cmd"
_ "golang.org/x/crypto/x509roots/fallback"
)
func main() {
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"group:allNonMajor",
"group:allDigest",
":disableDependencyDashboard",
":semanticCommitTypeAll(chore)"
],
"postUpdateOptions": [
"gomodUpdateImportPaths",
"gomodTidy"
],
"schedule": "* * * * 0,6"
}