Compare commits

...
15 Commits
Author SHA1 Message Date
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
14 changed files with 279 additions and 95 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23.x"
go-version: "1.24.x"
- run: go build .
env:
CGO_ENABLED: '0'
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23.x"
go-version: "1.24.x"
- uses: golangci/golangci-lint-action@v6
with:
version: "v1.60"
version: "v1.64"
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23.x"
go-version: "1.24.x"
- name: Run test with coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-alpine3.20 AS build
FROM golang:1.24-alpine3.20 AS build
ARG VERSION="untracked"
+12 -4
View File
@@ -49,6 +49,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.
@@ -139,6 +145,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
@@ -221,10 +229,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 =
```
@@ -249,6 +255,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
},
}
+16 -1
View File
@@ -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 {
+13 -18
View File
@@ -1,39 +1,34 @@
module github.com/hacdias/webdav/v5
go 1.23
go 1.24.0
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/go-viper/mapstructure/v2 v2.2.1
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/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.0
github.com/stretchr/testify v1.10.0
github.com/studio-b12/gowebdav v0.9.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.29.0
golang.org/x/net v0.31.0
golang.org/x/crypto v0.36.0
golang.org/x/net v0.37.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/hashicorp/hcl v1.0.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/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/sagikazarmark/locafero v0.8.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/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+26 -36
View File
@@ -1,5 +1,6 @@
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.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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=
@@ -8,20 +9,15 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -31,24 +27,22 @@ 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/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
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/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -59,20 +53,16 @@ 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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=
+1 -4
View File
@@ -40,7 +40,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 {
@@ -62,9 +62,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)
+14 -16
View File
@@ -2,7 +2,6 @@ package lib
import (
"net/http"
"net/url"
"os"
"strings"
@@ -116,21 +115,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr))
}
// 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 {
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(filename string) bool {
allowed := user.Allowed(req, func(filename string) bool {
_, err := user.FileSystem.Stat(r.Context(), filename)
return !os.IsNotExist(err)
})
@@ -152,8 +146,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"
@@ -183,5 +181,5 @@ type responseWriterNoBody struct {
}
func (w responseWriterNoBody) Write(data []byte) (int, error) {
return 0, nil
return len(data), nil
}
+76
View File
@@ -292,6 +292,82 @@ users:
require.ErrorContains(t, err, "403")
}
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()
+10 -11
View File
@@ -3,7 +3,6 @@ package lib
import (
"errors"
"fmt"
"net/http"
"path/filepath"
"regexp"
"strings"
@@ -51,12 +50,12 @@ type UserPermissions struct {
}
// 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
for i := len(p.Rules) - 1; i >= 0; i-- {
if p.Rules[i].Matches(dst) {
@@ -77,7 +76,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)
}
}
@@ -142,8 +141,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
@@ -152,7 +151,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
@@ -172,10 +171,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
}