mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-22 03:20:41 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af4c723f4 | ||
|
|
2406836223 | ||
|
|
7ca822bcaf | ||
|
|
a98d0c2616 | ||
|
|
98e92d0c13 | ||
|
|
53416e57da | ||
|
|
ff6637f396 | ||
|
|
15fbcd391b | ||
|
|
70b3630d08 | ||
|
|
aa014b092b | ||
|
|
b4553710eb | ||
|
|
e07e55d97a | ||
|
|
cfee0f9f0b | ||
|
|
79eb02aba4 | ||
|
|
7bbe36e4c1 | ||
|
|
520b7df04d | ||
|
|
8f3a3298a8 | ||
|
|
1753aa2945 | ||
|
|
4645f6f46f | ||
|
|
e998e2a838 | ||
|
|
3b3dedc4eb | ||
|
|
8706802dac | ||
|
|
5676a1c382 | ||
|
|
c938367dc0 | ||
|
|
758410ba64 | ||
|
|
1bbf52e475 | ||
|
|
2c2a325215 | ||
|
|
f7e006b449 | ||
|
|
49f61eab79 | ||
|
|
5d1a68c4f2 | ||
|
|
04cee682fb | ||
|
|
19d128cfbd | ||
|
|
118a0dda88 | ||
|
|
aad8e563a7 | ||
|
|
8446f9bdae | ||
|
|
5226853fca | ||
|
|
e350dc989a | ||
|
|
65de8e8578 | ||
|
|
f0b5a39d80 | ||
|
|
dbd708a76e | ||
|
|
f61b7cab3b | ||
|
|
3cfaa0da3e | ||
|
|
f7b78cd834 | ||
|
|
4b4e555ed5 | ||
|
|
6aeb3f8a30 | ||
|
|
59c5f1343f | ||
|
|
617496c019 | ||
|
|
3790d7de08 | ||
|
|
eaf42b03e9 | ||
|
|
dce6010b61 | ||
|
|
88863d7242 | ||
|
|
2c96db92aa | ||
|
|
79bc17afab | ||
|
|
51b101d3d8 | ||
|
|
ca7f3374d5 | ||
|
|
64bbdc7b15 | ||
|
|
d418bd2661 | ||
|
|
d500716f29 | ||
|
|
8c49af0b68 | ||
|
|
a5777e18ee | ||
|
|
49a6e935c3 | ||
|
|
a698e31cb4 | ||
|
|
74b514c877 | ||
|
|
ca0bdb1cfa | ||
|
|
a056e1ba18 | ||
|
|
189af88bc8 | ||
|
|
4e87e6a613 | ||
|
|
63449f1636 | ||
|
|
4ad26dad35 | ||
|
|
623bbc9a70 |
@@ -12,8 +12,10 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
go-version: "1.25.x"
|
||||
- run: go build .
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
go-version: "1.25.x"
|
||||
- uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: "v1.59"
|
||||
version: "latest"
|
||||
|
||||
@@ -12,10 +12,12 @@ jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.x"
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
go-version: "1.25.x"
|
||||
- name: Run test with coverage
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
+3
-1
@@ -8,10 +8,12 @@ before:
|
||||
builds:
|
||||
- main: main.go
|
||||
binary: webdav
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- '-trimpath'
|
||||
ldflags:
|
||||
- '-X github.com/hacdias/webdav/v5/cmd.version={{.Version}}'
|
||||
- '-s -w -X github.com/hacdias/webdav/v5/cmd.version={{.Version}}'
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-alpine3.20 AS build
|
||||
FROM golang:1.25-alpine3.22 AS build
|
||||
|
||||
ARG VERSION="untracked"
|
||||
|
||||
@@ -11,7 +11,7 @@ COPY ./go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . /webdav/
|
||||
RUN go build -o main -ldflags="-X 'github.com/hacdias/webdav/v5/cmd.version=$VERSION'" .
|
||||
RUN go build -o main -trimpath -ldflags="-s -w -X 'github.com/hacdias/webdav/v5/cmd.version=$VERSION'" .
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -49,6 +57,12 @@ docker run \
|
||||
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.
|
||||
@@ -71,6 +85,11 @@ debug: false
|
||||
# Disable sniffing the files to detect their content type. Default is 'false'.
|
||||
noSniff: false
|
||||
|
||||
# Whether the server runs behind a trusted proxy or not. When this is true,
|
||||
# the header X-Forwarded-For will be used for logging the remote addresses
|
||||
# of logging attempts (if available).
|
||||
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).
|
||||
@@ -81,9 +100,22 @@ directory: .
|
||||
# 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'.
|
||||
@@ -122,6 +154,8 @@ users:
|
||||
- username: admin
|
||||
password: admin
|
||||
# Example 'john' user with bcrypt encrypted password, with custom directory.
|
||||
# You can generate a bcrypt-encrypted password by using the 'webdav bcrypt'
|
||||
# command lint utility.
|
||||
- username: john
|
||||
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
||||
directory: /another/path
|
||||
@@ -133,14 +167,15 @@ users:
|
||||
# Override default permissions.
|
||||
permissions: CRUD
|
||||
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
|
||||
```
|
||||
@@ -156,7 +191,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 / {
|
||||
@@ -166,6 +203,32 @@ 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}
|
||||
|
||||
reverse_proxy 127.0.0.1:6065 { # if running on the same network in docker you can just point to the service name e.g. webdav:6065
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up REMOTE-HOST {remote_host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,6 +253,64 @@ Restart=on-failure
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Fail2Ban Setup
|
||||
|
||||
To add security against brute-force attacks in your WebDAV server, you can configure Fail2Ban to ban IP addresses after a set number of failed login attempts.
|
||||
|
||||
#### Filter Configuration
|
||||
|
||||
Create a new filter rule under `filter.d/webdav.conf`:
|
||||
|
||||
```ini
|
||||
[INCLUDES]
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
# Failregex to match "invalid password" and extract remote_address only
|
||||
failregex = ^.*invalid password\s*\{.*"remote_address":\s*"<HOST>:\d+"\s*\}
|
||||
^.*invalid username\s*\{.*"remote_address":\s*"<HOST>:\d+"\s*\}
|
||||
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
This configuration will capture invalid login attempts and extract the IP address to ban.
|
||||
|
||||
#### Jail Configuration
|
||||
|
||||
In `jail.d/webdav.conf`, define the jail that monitors your WebDAV log for failed login attempts:
|
||||
|
||||
```ini
|
||||
[webdav]
|
||||
|
||||
enabled = true
|
||||
port = [your_port]
|
||||
filter = webdav
|
||||
logpath = [your_log_path]
|
||||
banaction = iptables-allports
|
||||
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:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart fail2ban
|
||||
```
|
||||
|
||||
2. Verify that Fail2Ban is running and monitoring your WebDAV logs:
|
||||
|
||||
```bash
|
||||
sudo fail2ban-client status webdav
|
||||
```
|
||||
|
||||
With this setup, Fail2Ban will automatically block IP addresses that exceed the allowed number of failed login attempts.
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to open an issue or a pull request.
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
+16
-1
@@ -10,6 +10,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"
|
||||
@@ -115,7 +116,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 {
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
module github.com/hacdias/webdav/v5
|
||||
|
||||
go 1.22
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0
|
||||
github.com/rs/cors v1.11.0
|
||||
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
|
||||
github.com/coreos/go-systemd/v22 v22.6.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/spf13/cobra v1.10.1
|
||||
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.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/net v0.46.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.2 // 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.6.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-20240716175740-e3f259677ff7 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,65 +1,50 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
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.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
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.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
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.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
|
||||
github.com/studio-b12/gowebdav v0.11.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=
|
||||
@@ -68,21 +53,18 @@ 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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+25
-7
@@ -3,6 +3,7 @@ package lib
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -33,13 +34,14 @@ type Config struct {
|
||||
Prefix string
|
||||
NoSniff bool
|
||||
NoPassword bool
|
||||
BehindProxy bool
|
||||
Log Log
|
||||
CORS CORS
|
||||
Users []User
|
||||
}
|
||||
|
||||
func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
|
||||
v := viper.New()
|
||||
v := viper.NewWithOptions(viper.ExperimentalBindStruct())
|
||||
|
||||
// Configure flags bindings
|
||||
if flags != nil {
|
||||
@@ -52,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)
|
||||
@@ -61,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)
|
||||
@@ -74,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)
|
||||
@@ -114,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
|
||||
}
|
||||
}
|
||||
@@ -160,8 +178,8 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
for _, u := range c.Users {
|
||||
err := u.Validate(c.NoPassword)
|
||||
for i := range c.Users {
|
||||
err := c.Users[i].Validate(c.NoPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
+138
-10
@@ -22,6 +22,17 @@ func writeAndParseConfig(t *testing.T, content, extension string) *Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func writeAndParseConfigWithError(t *testing.T, content, extension, error string) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "config"+extension)
|
||||
|
||||
err := os.WriteFile(tmpFile, []byte(content), 0666)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ParseConfig(tmpFile, nil)
|
||||
require.ErrorContains(t, err, error)
|
||||
}
|
||||
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -181,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) {
|
||||
@@ -224,3 +322,33 @@ func TestConfigEnv(t *testing.T) {
|
||||
require.NoError(t, os.Setenv("WD_PERMISSIONS", ""))
|
||||
require.NoError(t, os.Setenv("WD_DIRECTORY", ""))
|
||||
}
|
||||
|
||||
func TestConfigParseUserPasswordEnvironment(t *testing.T) {
|
||||
content := `
|
||||
directory: /
|
||||
users:
|
||||
- username: '{env}USER1_USERNAME'
|
||||
password: '{env}USER1_PASSWORD'
|
||||
- username: basic
|
||||
password: basic
|
||||
`
|
||||
|
||||
writeAndParseConfigWithError(t, content, ".yml", "username environment variable is empty")
|
||||
|
||||
err := os.Setenv("USER1_USERNAME", "admin")
|
||||
require.NoError(t, err)
|
||||
|
||||
writeAndParseConfigWithError(t, content, ".yml", "password environment variable is empty")
|
||||
|
||||
err = os.Setenv("USER1_PASSWORD", "admin")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := writeAndParseConfig(t, content, ".yaml")
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
require.Equal(t, "admin", cfg.Users[0].Username)
|
||||
require.Equal(t, "basic", cfg.Users[1].Username)
|
||||
|
||||
require.True(t, cfg.Users[0].checkPassword("admin"))
|
||||
require.True(t, cfg.Users[1].checkPassword("basic"))
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+53
-20
@@ -2,7 +2,6 @@ package lib
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -17,14 +16,18 @@ type handlerUser struct {
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
noPassword bool
|
||||
user *handlerUser
|
||||
users map[string]*handlerUser
|
||||
noPassword bool
|
||||
behindProxy bool
|
||||
user *handlerUser
|
||||
users map[string]*handlerUser
|
||||
}
|
||||
|
||||
func NewHandler(c *Config) (http.Handler, error) {
|
||||
ls := webdav.NewMemLS()
|
||||
|
||||
h := &Handler{
|
||||
noPassword: c.NoPassword,
|
||||
noPassword: c.NoPassword,
|
||||
behindProxy: c.BehindProxy,
|
||||
user: &handlerUser{
|
||||
User: User{
|
||||
UserPermissions: c.UserPermissions,
|
||||
@@ -35,7 +38,10 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
users: map[string]*handlerUser{},
|
||||
@@ -50,7 +56,10 @@ 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -61,6 +70,7 @@ func NewHandler(c *Config) (http.Handler, error) {
|
||||
AllowedOrigins: c.CORS.AllowedHosts,
|
||||
AllowedMethods: c.CORS.AllowedMethods,
|
||||
AllowedHeaders: c.CORS.AllowedHeaders,
|
||||
ExposedHeaders: c.CORS.ExposedHeaders,
|
||||
OptionsPassthrough: false,
|
||||
}).Handler(h), nil
|
||||
}
|
||||
@@ -84,9 +94,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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()
|
||||
zap.L().Info("login attempt", zap.String("username", username), zap.String("remote_address", r.RemoteAddr))
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -94,27 +106,34 @@ 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))
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.noPassword && !user.checkPassword(password) {
|
||||
zap.L().Info("invalid password", zap.String("username", username), zap.String("remote_address", r.RemoteAddr))
|
||||
// Log invalid password
|
||||
zap.L().Info("invalid password", zap.String("username", username), zap.String("remote_address", remoteAddr))
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("user authorized", zap.String("username", username))
|
||||
// Log successful authorization
|
||||
zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr))
|
||||
}
|
||||
|
||||
// Convert the HTTP request into an internal request type
|
||||
req, err := newRequest(r, h.user.Prefix)
|
||||
if err != nil {
|
||||
zap.L().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(destination string) bool {
|
||||
u, err := url.Parse(destination)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
path := strings.TrimPrefix(u.Path, user.Prefix)
|
||||
_, err = user.FileSystem.Stat(r.Context(), path)
|
||||
allowed := user.Allowed(req, func(filename string) bool {
|
||||
_, err := user.FileSystem.Stat(r.Context(), filename)
|
||||
return !os.IsNotExist(err)
|
||||
})
|
||||
|
||||
@@ -135,8 +154,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"
|
||||
@@ -151,10 +174,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
user.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getRealRemoteIP retrieves the client's actual IP address, considering reverse proxies.
|
||||
func getRealRemoteIP(r *http.Request, behindProxy bool) string {
|
||||
if behindProxy {
|
||||
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
type responseWriterNoBody struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
+138
-4
@@ -217,11 +217,12 @@ users:
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerRules(t *testing.T) {
|
||||
func TestServerRulesRestrictive(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"),
|
||||
@@ -240,11 +241,11 @@ users:
|
||||
rules:
|
||||
- regex: "^.+.js$"
|
||||
permissions: R
|
||||
- path: "/b"
|
||||
- path: "/b/"
|
||||
permissions: R
|
||||
- path: "/a/foo.txt"
|
||||
permissions: none
|
||||
- path: "/c"
|
||||
- path: "/c/"
|
||||
permissions: none
|
||||
`, dir))
|
||||
|
||||
@@ -252,7 +253,7 @@ users:
|
||||
|
||||
files, err := client.ReadDir("/")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, files, 4)
|
||||
require.Len(t, files, 5)
|
||||
|
||||
err = client.Write("/foo.txt", []byte("new"), 0666)
|
||||
require.NoError(t, err)
|
||||
@@ -260,6 +261,18 @@ users:
|
||||
err = client.Write("/new.txt", []byte("new"), 0666)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Copy("/bar.js", "/b/bar.js", false)
|
||||
require.ErrorContains(t, err, "403")
|
||||
|
||||
err = client.Copy("/bar.js", "/bar.jsx", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Copy("/b/foo.txt", "/foo1.txt", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Rename("/b/foo.txt", "/foo2.txt", false)
|
||||
require.ErrorContains(t, err, "403")
|
||||
|
||||
_, err = client.Read("/a/foo.txt")
|
||||
require.ErrorContains(t, err, "403")
|
||||
|
||||
@@ -279,6 +292,127 @@ users:
|
||||
require.ErrorContains(t, err, "403")
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+76
-20
@@ -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,24 +35,55 @@ 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, destinationExists func(string) bool) bool {
|
||||
// Go through rules beginning from the last one.
|
||||
for i := len(p.Rules) - 1; i >= 0; i-- {
|
||||
rule := p.Rules[i]
|
||||
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.destination
|
||||
ruleMatched := false
|
||||
|
||||
if rule.Matches(r.URL.Path) {
|
||||
return rule.Permissions.Allowed(r, destinationExists)
|
||||
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
|
||||
}
|
||||
|
||||
// Only check the first rule that matches, similarly to the source rules.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ruleMatched && !p.Permissions.AllowedDestination(r, fileExists) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return p.Permissions.Allowed(r, destinationExists)
|
||||
// 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.path) {
|
||||
return p.Rules[i].Permissions.Allowed(r, fileExists)
|
||||
}
|
||||
}
|
||||
|
||||
return p.Permissions.Allowed(r, fileExists)
|
||||
}
|
||||
|
||||
func (p *UserPermissions) Validate() error {
|
||||
@@ -66,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
|
||||
}
|
||||
|
||||
@@ -100,8 +141,10 @@ func (p *Permissions) UnmarshalText(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Permissions) Allowed(r *http.Request, destinationExists func(string) bool) bool {
|
||||
switch r.Method {
|
||||
// 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 *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
|
||||
@@ -110,17 +153,15 @@ func (p Permissions) Allowed(r *http.Request, destinationExists func(string) boo
|
||||
case "PROPPATCH":
|
||||
return p.Update
|
||||
case "PUT":
|
||||
if destinationExists(r.URL.Path) {
|
||||
return p.Update
|
||||
} else {
|
||||
return p.Create
|
||||
}
|
||||
case "COPY", "MOVE":
|
||||
if destinationExists(r.Header.Get("Destination")) {
|
||||
if fileExists(r.path) {
|
||||
return p.Update
|
||||
} else {
|
||||
return p.Create
|
||||
}
|
||||
case "COPY":
|
||||
return p.Read
|
||||
case "MOVE":
|
||||
return p.Read && p.Delete
|
||||
case "DELETE":
|
||||
return p.Delete
|
||||
case "LOCK", "UNLOCK":
|
||||
@@ -129,3 +170,18 @@ func (p Permissions) Allowed(r *http.Request, destinationExists func(string) boo
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *request, fileExists func(string) bool) bool {
|
||||
switch r.method {
|
||||
case "COPY", "MOVE":
|
||||
if fileExists(r.destination) {
|
||||
return p.Update
|
||||
} else {
|
||||
return p.Create
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+10
-1
@@ -27,12 +27,21 @@ func (u User) checkPassword(input string) bool {
|
||||
func (u *User) Validate(noPassword bool) error {
|
||||
if u.Username == "" {
|
||||
return errors.New("invalid user: username must be set")
|
||||
} else if strings.HasPrefix(u.Username, "{env}") {
|
||||
env := strings.TrimPrefix(u.Username, "{env}")
|
||||
if env == "" {
|
||||
return fmt.Errorf("invalid user %q: username environment variable not set", u.Username)
|
||||
}
|
||||
|
||||
u.Username = os.Getenv(env)
|
||||
if u.Username == "" {
|
||||
return fmt.Errorf("invalid user %q: username environment variable is empty", u.Username)
|
||||
}
|
||||
}
|
||||
|
||||
if u.Password == "" && !noPassword {
|
||||
return fmt.Errorf("invalid user %q: password must be set", u.Username)
|
||||
} else if strings.HasPrefix(u.Password, "{env}") {
|
||||
|
||||
env := strings.TrimPrefix(u.Password, "{env}")
|
||||
if env == "" {
|
||||
return fmt.Errorf("invalid user %q: password environment variable not set", u.Username)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"group:allNonMajor",
|
||||
"group:allDigest",
|
||||
":disableDependencyDashboard"
|
||||
],
|
||||
"postUpdateOptions": [
|
||||
"gomodUpdateImportPaths",
|
||||
"gomodTidy"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user