mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-23 19:51:53 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617496c019 | ||
|
|
3790d7de08 | ||
|
|
eaf42b03e9 |
@@ -49,6 +49,12 @@ docker run \
|
|||||||
ghcr.io/hacdias/webdav -c /config.yml
|
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
|
## Configuration
|
||||||
|
|
||||||
The configuration can be provided as a YAML, JSON or TOML file. Below is an example of a YAML configuration file with all the options available, as well as what they mean.
|
The configuration can be provided as a YAML, JSON or TOML file. Below is an example of a YAML configuration file with all the options available, as well as what they mean.
|
||||||
@@ -139,6 +145,8 @@ users:
|
|||||||
- username: admin
|
- username: admin
|
||||||
password: admin
|
password: admin
|
||||||
# Example 'john' user with bcrypt encrypted password, with custom directory.
|
# 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
|
- username: john
|
||||||
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
||||||
directory: /another/path
|
directory: /another/path
|
||||||
@@ -221,10 +229,8 @@ before = common.conf
|
|||||||
|
|
||||||
[Definition]
|
[Definition]
|
||||||
# Failregex to match "invalid password" and extract remote_address only
|
# Failregex to match "invalid password" and extract remote_address only
|
||||||
failregex = ^.*invalid password\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*\}
|
||||||
# Failregex to match "invalid username" and extract remote_address only (if applicable)
|
|
||||||
failregex += ^.*invalid username\s*\{.*"remote_address":\s*"<HOST>"\s*\}
|
|
||||||
|
|
||||||
ignoreregex =
|
ignoreregex =
|
||||||
```
|
```
|
||||||
@@ -249,6 +255,8 @@ ignoreself = false
|
|||||||
- Replace `[your_port]` with the port your WebDAV server is running on.
|
- Replace `[your_port]` with the port your WebDAV server is running on.
|
||||||
- Replace `[your_log_path]` with the path to your WebDAV log file.
|
- 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
|
#### Final Steps
|
||||||
|
|
||||||
1. Restart Fail2Ban to apply these configurations:
|
1. Restart Fail2Ban to apply these configurations:
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
+39
-9
@@ -19,6 +19,7 @@ type handlerUser struct {
|
|||||||
type Handler struct {
|
type Handler struct {
|
||||||
noPassword bool
|
noPassword bool
|
||||||
behindProxy bool
|
behindProxy bool
|
||||||
|
prefix string
|
||||||
user *handlerUser
|
user *handlerUser
|
||||||
users map[string]*handlerUser
|
users map[string]*handlerUser
|
||||||
}
|
}
|
||||||
@@ -27,12 +28,12 @@ func NewHandler(c *Config) (http.Handler, error) {
|
|||||||
h := &Handler{
|
h := &Handler{
|
||||||
noPassword: c.NoPassword,
|
noPassword: c.NoPassword,
|
||||||
behindProxy: c.BehindProxy,
|
behindProxy: c.BehindProxy,
|
||||||
|
prefix: c.Prefix,
|
||||||
user: &handlerUser{
|
user: &handlerUser{
|
||||||
User: User{
|
User: User{
|
||||||
UserPermissions: c.UserPermissions,
|
UserPermissions: c.UserPermissions,
|
||||||
},
|
},
|
||||||
Handler: webdav.Handler{
|
Handler: webdav.Handler{
|
||||||
Prefix: c.Prefix,
|
|
||||||
FileSystem: Dir{
|
FileSystem: Dir{
|
||||||
Dir: webdav.Dir(c.Directory),
|
Dir: webdav.Dir(c.Directory),
|
||||||
noSniff: c.NoSniff,
|
noSniff: c.NoSniff,
|
||||||
@@ -47,7 +48,6 @@ func NewHandler(c *Config) (http.Handler, error) {
|
|||||||
h.users[u.Username] = &handlerUser{
|
h.users[u.Username] = &handlerUser{
|
||||||
User: u,
|
User: u,
|
||||||
Handler: webdav.Handler{
|
Handler: webdav.Handler{
|
||||||
Prefix: c.Prefix,
|
|
||||||
FileSystem: Dir{
|
FileSystem: Dir{
|
||||||
Dir: webdav.Dir(u.Directory),
|
Dir: webdav.Dir(u.Directory),
|
||||||
noSniff: c.NoSniff,
|
noSniff: c.NoSniff,
|
||||||
@@ -116,19 +116,49 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr))
|
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
|
// Validate and clean destination header if it exists, by stripping out the
|
||||||
// and only keeping the path.
|
// prefix and only keeping the actual destination path, always prefixed by
|
||||||
|
// a forward slash to ensure that the rules can successful match the path.
|
||||||
if destination := r.Header.Get("Destination"); destination != "" {
|
if destination := r.Header.Get("Destination"); destination != "" {
|
||||||
u, err := url.Parse(destination)
|
u, err := url.Parse(destination)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
destination = strings.TrimPrefix(u.Path, user.Prefix)
|
http.Error(w, "Invalid Destination header", http.StatusBadRequest)
|
||||||
if !strings.HasPrefix(destination, "/") {
|
return
|
||||||
destination = "/" + destination
|
}
|
||||||
|
|
||||||
|
if h.prefix != "" {
|
||||||
|
destination = strings.TrimPrefix(u.Path, h.prefix)
|
||||||
|
if len(destination) >= len(u.Path) {
|
||||||
|
http.Error(w, "Invalid URL prefix", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
r.Header.Set("Destination", destination)
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(destination, "/") {
|
||||||
|
destination = "/" + destination
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("Destination", destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up URL path by stripping out the prefix, and ensuring it always begins
|
||||||
|
// with a forward slash, so that it can match against the rules.
|
||||||
|
path := r.URL.Path
|
||||||
|
|
||||||
|
if h.prefix != "" {
|
||||||
|
path = strings.TrimPrefix(r.URL.Path, h.prefix)
|
||||||
|
if len(path) >= len(r.URL.Path) {
|
||||||
|
http.Error(w, "Invalid URL prefix", http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
r.URL.Path = path
|
||||||
|
|
||||||
// Checks for user permissions relatively to this PATH.
|
// Checks for user permissions relatively to this PATH.
|
||||||
allowed := user.Allowed(r, func(filename string) bool {
|
allowed := user.Allowed(r, func(filename string) bool {
|
||||||
_, err := user.FileSystem.Stat(r.Context(), filename)
|
_, err := user.FileSystem.Stat(r.Context(), filename)
|
||||||
|
|||||||
@@ -292,6 +292,82 @@ users:
|
|||||||
require.ErrorContains(t, err, "403")
|
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) {
|
func TestServerPermissions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user