Compare commits

...
Author SHA1 Message Date
renovate[bot] 010ca576fb chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 7dacc38 2025-12-13 09:10:00 +01:00
renovate[bot] 7b8aa1c422 chore(deps): update all non-major dependencies 2025-12-13 09:05:43 +01:00
Henrique Dias 2f98af7d1e ci: run renovate on weekends 2025-12-05 15:10:32 +01:00
renovate[bot] 50e1828b60 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to 3a1c6b4 2025-12-05 14:34:40 +01:00
renovate[bot] e78d59e374 chore(deps): update module github.com/spf13/cobra to v1.10.2 2025-12-05 14:31:30 +01:00
renovate[bot] de0a102ed8 chore(deps): update golang.org/x/crypto/x509roots/fallback digest to f4602e4 2025-12-03 08:46:31 +01:00
Henrique Dias bdfa98d00d feat: use golang.org/x/crypto/x509roots/fallback 2025-11-20 19:12:12 +01:00
renovate[bot] f0e7e23ea8 chore(deps): update actions/checkout action to v6 2025-11-20 18:55:14 +01:00
renovate[bot] 065991a195 chore(deps): update all non-major dependencies 2025-11-20 08:09:56 +01:00
Henrique Dias bb85514c8f chore: use 'chore' in all commits from renovate 2025-11-13 14:11:51 +01:00
renovate[bot] c0af549a1f fix(deps): update all non-major dependencies 2025-11-12 07:19:53 +01:00
Emmanuel Ferdman 685d89162a chore: update GoReleaser configurations
Signed-off-by: Emmanuel Ferdman <[email protected]>
2025-11-10 17:17:48 +01:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a5496c9516 chore(deps): update golangci/golangci-lint-action action to v9 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 07:36:01 +01:00
Michael Bruns 24abd52e23 feat: enhanced logging 2025-10-30 07:55:50 +01:00
Henrique Dias 1af4c723f4 ci: fix releaser go version 2025-10-17 17:21:51 +02:00
Henrique Dias 2406836223 fix: additive permissions 2025-10-17 17:12:11 +02:00
14 changed files with 110 additions and 41 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
+2 -2
View File
@@ -12,10 +12,10 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- uses: golangci/golangci-lint-action@v8
- uses: golangci/golangci-lint-action@v9
with:
version: "latest"
+3 -1
View File
@@ -12,10 +12,12 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
+4 -2
View File
@@ -44,10 +44,12 @@ builds:
archives:
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
format: tar.gz
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
release:
github:
-3
View File
@@ -2,8 +2,6 @@ FROM golang:1.25-alpine3.22 AS build
ARG VERSION="untracked"
RUN apk --update add ca-certificates
WORKDIR /webdav/
COPY ./go.mod ./
@@ -15,7 +13,6 @@ RUN go build -o main -trimpath -ldflags="-s -w -X 'github.com/hacdias/webdav/v5/
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /webdav/main /bin/webdav
EXPOSE 6065
+7 -6
View File
@@ -6,14 +6,15 @@ require (
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/cobra v1.10.2
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.43.0
golang.org/x/net v0.46.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.46.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00
golang.org/x/net v0.48.0
)
require (
@@ -29,7 +30,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.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
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+14 -12
View File
@@ -34,8 +34,8 @@ 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/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
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=
@@ -51,18 +51,20 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 h1:qObov2/X4yIpr98j5t6samg3mMF12Rl4taUJd1rWj+c=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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=
+22 -8
View File
@@ -25,6 +25,11 @@ type Handler struct {
func NewHandler(c *Config) (http.Handler, error) {
ls := webdav.NewMemLS()
logFunc := func(r *http.Request, err error) {
lZap := getRequestLogger(r, c.BehindProxy)
lZap.Debug("handle webdav request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Error(err))
}
h := &Handler{
noPassword: c.NoPassword,
behindProxy: c.BehindProxy,
@@ -42,6 +47,7 @@ func NewHandler(c *Config) (http.Handler, error) {
LockSystem: ls,
directory: c.Directory,
},
Logger: logFunc,
},
},
users: map[string]*handlerUser{},
@@ -60,6 +66,7 @@ func NewHandler(c *Config) (http.Handler, error) {
LockSystem: ls,
directory: u.Directory,
},
Logger: logFunc,
},
}
}
@@ -90,13 +97,12 @@ func NewHandler(c *Config) (http.Handler, error) {
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user := h.user
lZap := getRequestLogger(r, h.behindProxy)
// Authentication
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()
if !ok {
@@ -107,26 +113,26 @@ 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))
lZap.Info("invalid username", zap.String("username", username))
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
if !h.noPassword && !user.checkPassword(password) {
// Log invalid password
zap.L().Info("invalid password", zap.String("username", username), zap.String("remote_address", remoteAddr))
lZap.Info("invalid password", zap.String("username", username))
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
// Log successful authorization
zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr))
lZap.Info("user authorized", zap.String("username", username))
}
// 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))
lZap.Info("invalid request path or destination", zap.Error(err))
http.Error(w, "Invalid request path or destination", http.StatusBadRequest)
return
}
@@ -137,7 +143,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return !os.IsNotExist(err)
})
zap.L().Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))
lZap.Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))
if !allowed {
w.WriteHeader(http.StatusForbidden)
@@ -174,6 +180,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user.ServeHTTP(w, r)
}
// getRequestLogger creates a zap.Logger using the request remote ip.
func getRequestLogger(r *http.Request, behindProxy bool) *zap.Logger {
// Retrieve the real client IP address using the updated helper function
remoteAddr := getRealRemoteIP(r, behindProxy)
return zap.L().With(zap.String("remote_address", remoteAddr))
}
// getRealRemoteIP retrieves the client's actual IP address, considering reverse proxies.
func getRealRemoteIP(r *http.Request, behindProxy bool) string {
if behindProxy {
+46 -1
View File
@@ -217,7 +217,7 @@ users:
})
}
func TestServerRules(t *testing.T) {
func TestServerRulesRestrictive(t *testing.T) {
t.Parallel()
dir := makeTestDirectory(t, map[string][]byte{
@@ -292,6 +292,51 @@ 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()
+3 -1
View File
@@ -56,9 +56,11 @@ func (p UserPermissions) Allowed(r *request, fileExists func(string) bool) bool
// we fail immediately. If no rule matches, we check the global permissions.
if r.method == "COPY" || r.method == "MOVE" {
dst := r.destination
ruleMatched := false
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
}
@@ -68,7 +70,7 @@ func (p UserPermissions) Allowed(r *request, fileExists func(string) bool) bool
}
}
if !p.Permissions.AllowedDestination(r, fileExists) {
if !ruleMatched && !p.Permissions.AllowedDestination(r, fileExists) {
return false
}
}
+2
View File
@@ -2,6 +2,8 @@ package main
import (
"github.com/hacdias/webdav/v5/cmd"
_ "golang.org/x/crypto/x509roots/fallback"
)
func main() {
+4 -2
View File
@@ -4,10 +4,12 @@
"config:recommended",
"group:allNonMajor",
"group:allDigest",
":disableDependencyDashboard"
":disableDependencyDashboard",
":semanticCommitTypeAll(chore)"
],
"postUpdateOptions": [
"gomodUpdateImportPaths",
"gomodTidy"
]
],
"schedule": "* * * * 0,6"
}