Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
eaf42b03e9 | ||
![]() |
dce6010b61 | ||
![]() |
88863d7242 | ||
![]() |
2c96db92aa | ||
![]() |
79bc17afab | ||
![]() |
51b101d3d8 | ||
![]() |
ca7f3374d5 | ||
![]() |
64bbdc7b15 | ||
![]() |
d418bd2661 | ||
![]() |
d500716f29 | ||
![]() |
8c49af0b68 |
11 changed files with 318 additions and 55 deletions
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -18,4 +18,4 @@ jobs:
|
|||
go-version: "1.23.x"
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: "v1.60"
|
||||
version: "v1.62"
|
||||
|
|
21
README.md
21
README.md
|
@ -71,6 +71,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 +86,21 @@ 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.
|
||||
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 +139,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
|
||||
|
|
49
cmd/bcrypt.go
Normal file
49
cmd/bcrypt.go
Normal 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
|
||||
},
|
||||
}
|
17
cmd/root.go
17
cmd/root.go
|
@ -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 {
|
||||
|
|
17
go.mod
17
go.mod
|
@ -3,6 +3,7 @@ module github.com/hacdias/webdav/v5
|
|||
go 1.23
|
||||
|
||||
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
|
||||
|
@ -11,16 +12,16 @@ require (
|
|||
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
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/net v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // 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/magiconair/properties v1.8.9 // 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
|
||||
|
@ -28,12 +29,12 @@ require (
|
|||
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/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-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
35
go.sum
35
go.sum
|
@ -1,13 +1,16 @@
|
|||
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.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
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=
|
||||
|
@ -18,8 +21,8 @@ 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/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/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=
|
||||
|
@ -39,8 +42,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS
|
|||
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/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/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=
|
||||
|
@ -59,16 +62,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.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=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
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=
|
||||
|
|
|
@ -33,6 +33,7 @@ type Config struct {
|
|||
Prefix string
|
||||
NoSniff bool
|
||||
NoPassword bool
|
||||
BehindProxy bool
|
||||
Log Log
|
||||
CORS CORS
|
||||
Users []User
|
||||
|
@ -74,6 +75,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 +116,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 +176,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)
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -17,14 +17,16 @@ 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) {
|
||||
h := &Handler{
|
||||
noPassword: c.NoPassword,
|
||||
noPassword: c.NoPassword,
|
||||
behindProxy: c.BehindProxy,
|
||||
user: &handlerUser{
|
||||
User: User{
|
||||
UserPermissions: c.UserPermissions,
|
||||
|
@ -61,6 +63,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
|
||||
}
|
||||
|
@ -85,7 +88,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
// Retrieve the real client IP address using the updated helper function
|
||||
remoteAddr := getRealRemoteIP(r)
|
||||
remoteAddr := getRealRemoteIP(r, h.behindProxy)
|
||||
|
||||
// Gets the correct user for this request.
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
@ -166,12 +169,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// getRealRemoteIP retrieves the client's actual IP address, considering reverse proxies.
|
||||
func getRealRemoteIP(r *http.Request) string {
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
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 {
|
||||
|
|
|
@ -16,6 +16,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,10 +36,18 @@ 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
|
||||
|
@ -87,6 +99,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
|
||||
}
|
||||
|
||||
|
|
11
lib/user.go
11
lib/user.go
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue