Files
warpbox-dev/backend/libs/services/storage_webdav.go
Daniel Legt 61b7c283a4 fix(auth): reject invalid bearer tokens instead of falling back
Modify the authentication handler to return an unauthorized error when
an invalid or disabled bearer token is provided, rather than silently
falling back to an anonymous request.

This ensures that clients attempting to authenticate but failing (due to
expired, malformed, or disabled tokens) are explicitly notified of the
auth failure instead of proceeding anonymously. True anonymous requests
without any Authorization header remain supported.
2026-05-31 13:02:58 +03:00

194 lines
5.4 KiB
Go

package services
import (
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"path"
"strings"
"time"
)
type webDAVStorageBackend struct {
cfg StorageBackendConfig
client *http.Client
}
func (b webDAVStorageBackend) ID() string { return b.cfg.ID }
func (b webDAVStorageBackend) Type() string { return StorageBackendWebDAV }
func (b webDAVStorageBackend) Put(ctx context.Context, key string, body io.Reader, _ int64, contentType string) error {
if err := b.mkcolParents(ctx, key); err != nil {
return err
}
request, err := b.request(ctx, http.MethodPut, key, body)
if err != nil {
return err
}
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
response, err := b.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("webdav put failed: %s", response.Status)
}
return nil
}
func (b webDAVStorageBackend) Get(ctx context.Context, key string) (StorageObject, error) {
request, err := b.request(ctx, http.MethodGet, key, nil)
if err != nil {
return StorageObject{}, err
}
response, err := b.client.Do(request)
if err != nil {
return StorageObject{}, err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
response.Body.Close()
return StorageObject{}, fmt.Errorf("webdav get failed: %s", response.Status)
}
modTime, _ := time.Parse(http.TimeFormat, response.Header.Get("Last-Modified"))
return StorageObject{Key: key, Size: response.ContentLength, ContentType: response.Header.Get("Content-Type"), ModTime: modTime, Body: response.Body}, nil
}
func (b webDAVStorageBackend) Delete(ctx context.Context, key string) error {
return b.deletePath(ctx, key)
}
func (b webDAVStorageBackend) DeletePrefix(ctx context.Context, prefix string) error {
return b.deletePath(ctx, strings.TrimSuffix(prefix, "/")+"/")
}
func (b webDAVStorageBackend) Usage(ctx context.Context) (int64, error) {
request, err := b.request(ctx, "PROPFIND", "", nil)
if err != nil {
return 0, err
}
request.Header.Set("Depth", "infinity")
request.Header.Set("Content-Type", "application/xml")
response, err := b.client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return 0, fmt.Errorf("webdav usage failed: %s", response.Status)
}
var multi webDAVMultiStatus
if err := xml.NewDecoder(response.Body).Decode(&multi); err != nil {
return 0, err
}
var total int64
for _, item := range multi.Responses {
if item.PropStat.Prop.ResourceType.Collection != nil {
continue
}
total += item.PropStat.Prop.ContentLength
}
return total, nil
}
func (b webDAVStorageBackend) Test(ctx context.Context) error {
key := ".warpbox-storage-test-" + randomID(6)
if err := b.Put(ctx, key, strings.NewReader("ok"), 2, "text/plain"); err != nil {
return err
}
return b.Delete(ctx, key)
}
func (b webDAVStorageBackend) deletePath(ctx context.Context, key string) error {
request, err := b.request(ctx, http.MethodDelete, key, nil)
if err != nil {
return err
}
response, err := b.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return nil
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("webdav delete failed: %s", response.Status)
}
return nil
}
func (b webDAVStorageBackend) mkcolParents(ctx context.Context, key string) error {
dir := path.Dir(cleanObjectKey(key))
if dir == "." || dir == "/" {
return nil
}
parts := strings.Split(strings.Trim(dir, "/"), "/")
current := ""
for _, part := range parts {
current = path.Join(current, part)
request, err := b.request(ctx, "MKCOL", strings.TrimSuffix(current, "/")+"/", nil)
if err != nil {
return err
}
response, err := b.client.Do(request)
if err != nil {
return err
}
response.Body.Close()
if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusMethodNotAllowed && response.StatusCode != http.StatusConflict {
return fmt.Errorf("webdav mkcol failed: %s", response.Status)
}
}
return nil
}
func (b webDAVStorageBackend) request(ctx context.Context, method, key string, body io.Reader) (*http.Request, error) {
endpoint := strings.TrimRight(b.cfg.Endpoint, "/")
if endpoint == "" {
return nil, fmt.Errorf("webdav url is required")
}
remote := path.Join(cleanRemoteRoot(b.cfg.RemotePath), cleanObjectKey(key))
if strings.HasSuffix(key, "/") && !strings.HasSuffix(remote, "/") {
remote += "/"
}
target := endpoint + "/" + strings.TrimLeft(remote, "/")
request, err := http.NewRequestWithContext(ctx, method, target, body)
if err != nil {
return nil, err
}
if b.cfg.Username != "" || b.cfg.Password != "" {
request.SetBasicAuth(b.cfg.Username, b.cfg.Password)
}
return request, nil
}
type webDAVMultiStatus struct {
Responses []webDAVResponse `xml:"response"`
}
type webDAVResponse struct {
PropStat webDAVPropStat `xml:"propstat"`
}
type webDAVPropStat struct {
Prop webDAVProp `xml:"prop"`
}
type webDAVProp struct {
ContentLength int64 `xml:"getcontentlength"`
ResourceType webDAVResourceType `xml:"resourcetype"`
}
type webDAVResourceType struct {
Collection *struct{} `xml:"collection"`
}
func newWebDAVStorageBackend(cfg StorageBackendConfig) webDAVStorageBackend {
return webDAVStorageBackend{cfg: cfg, client: http.DefaultClient}
}