mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-24 04:01:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61b7cab3b | ||
|
|
3cfaa0da3e | ||
|
|
f7b78cd834 | ||
|
|
4b4e555ed5 | ||
|
|
6aeb3f8a30 | ||
|
|
59c5f1343f | ||
|
|
617496c019 | ||
|
|
3790d7de08 |
@@ -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.
|
||||||
@@ -223,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 =
|
||||||
```
|
```
|
||||||
@@ -251,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:
|
||||||
|
|||||||
+14
-16
@@ -2,7 +2,6 @@ package lib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"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))
|
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
|
// Convert the HTTP request into an internal request type
|
||||||
// and only keeping the path.
|
req, err := newRequest(r, h.user.Prefix)
|
||||||
if destination := r.Header.Get("Destination"); destination != "" {
|
if err != nil {
|
||||||
u, err := url.Parse(destination)
|
zap.L().Info("invalid request path or destination", zap.Error(err))
|
||||||
if err == nil {
|
http.Error(w, "Invalid request path or destination", http.StatusBadRequest)
|
||||||
destination = strings.TrimPrefix(u.Path, user.Prefix)
|
return
|
||||||
if !strings.HasPrefix(destination, "/") {
|
|
||||||
destination = "/" + destination
|
|
||||||
}
|
|
||||||
r.Header.Set("Destination", destination)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(req, func(filename string) bool {
|
||||||
_, err := user.FileSystem.Stat(r.Context(), filename)
|
_, err := user.FileSystem.Stat(r.Context(), filename)
|
||||||
return !os.IsNotExist(err)
|
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
|
// "index.html" resource, a human-readable view of the contents of
|
||||||
// the collection, or something else altogether.
|
// the collection, or something else altogether.
|
||||||
//
|
//
|
||||||
// Get, when applied to collection, will return the same as PROPFIND method.
|
// Similarly, since the definition of HEAD is a GET without a response
|
||||||
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, user.Prefix) {
|
// 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))
|
info, err := user.FileSystem.Stat(r.Context(), strings.TrimPrefix(r.URL.Path, user.Prefix))
|
||||||
if err == nil && info.IsDir() {
|
if err == nil && info.IsDir() {
|
||||||
r.Method = "PROPFIND"
|
r.Method = "PROPFIND"
|
||||||
@@ -183,5 +181,5 @@ type responseWriterNoBody struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||||
return 0, nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
+10
-11
@@ -3,7 +3,6 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -51,12 +50,12 @@ type UserPermissions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allowed checks if the user has permission to access a directory/file
|
// 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
|
// 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,
|
// 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.
|
// we fail immediately. If no rule matches, we check the global permissions.
|
||||||
if r.Method == "COPY" || r.Method == "MOVE" {
|
if r.method == "COPY" || r.method == "MOVE" {
|
||||||
dst := r.Header.Get("Destination")
|
dst := r.destination
|
||||||
|
|
||||||
for i := len(p.Rules) - 1; i >= 0; i-- {
|
for i := len(p.Rules) - 1; i >= 0; i-- {
|
||||||
if p.Rules[i].Matches(dst) {
|
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
|
// Go through rules beginning from the last one, and check the permissions at
|
||||||
// the source. The first matched rule returns.
|
// the source. The first matched rule returns.
|
||||||
for i := len(p.Rules) - 1; i >= 0; i-- {
|
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)
|
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
|
// Allowed returns whether this permission set has permissions to execute this
|
||||||
// request in the source directory. This applies to all requests with all methods.
|
// 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 {
|
func (p Permissions) Allowed(r *request, fileExists func(string) bool) bool {
|
||||||
switch r.Method {
|
switch r.method {
|
||||||
case "GET", "HEAD", "OPTIONS", "POST", "PROPFIND":
|
case "GET", "HEAD", "OPTIONS", "POST", "PROPFIND":
|
||||||
// Note: POST backend implementation just returns the same thing as GET.
|
// Note: POST backend implementation just returns the same thing as GET.
|
||||||
return p.Read
|
return p.Read
|
||||||
@@ -152,7 +151,7 @@ func (p Permissions) Allowed(r *http.Request, fileExists func(string) bool) bool
|
|||||||
case "PROPPATCH":
|
case "PROPPATCH":
|
||||||
return p.Update
|
return p.Update
|
||||||
case "PUT":
|
case "PUT":
|
||||||
if fileExists(r.URL.Path) {
|
if fileExists(r.path) {
|
||||||
return p.Update
|
return p.Update
|
||||||
} else {
|
} else {
|
||||||
return p.Create
|
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
|
// AllowedDestination returns whether this permissions set has permissions to execute this
|
||||||
// request in the destination directory. This only applies for COPY and MOVE requests.
|
// 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 {
|
func (p Permissions) AllowedDestination(r *request, fileExists func(string) bool) bool {
|
||||||
switch r.Method {
|
switch r.method {
|
||||||
case "COPY", "MOVE":
|
case "COPY", "MOVE":
|
||||||
if fileExists(r.Header.Get("Destination")) {
|
if fileExists(r.destination) {
|
||||||
return p.Update
|
return p.Update
|
||||||
} else {
|
} else {
|
||||||
return p.Create
|
return p.Create
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user