Compare commits

..

10 Commits

Author SHA1 Message Date
173b2cc388 update readme 2025-09-01 13:37:00 +02:00
oliverpool
1d5a5dcd6c carddav: acl readonly (for Thunderbird) 2025-09-01 13:36:06 +02:00
oliverpool
9c900b1c66 caldav, carddav: redirect on wrong path
it is much more user-friendly to redirect to the correct
principalPath/homeSetPath when possible
2025-09-01 13:36:06 +02:00
Krystian Chachuła
e4babc2798 webdav: handle permission errors in ReadDir 2025-08-28 18:25:37 +02:00
Krystian Chachuła
87062437b6 webdav: return multistatus with 403 on PROPPATCH
This fixes a "The file cannot be accessed by the system" error when uploading
some files with Windows Explorer.
2025-07-09 01:28:14 +02:00
Jonathan Liu
75c185517e caldav: add expand request to client 2025-03-16 15:41:33 +01:00
krystiancha
b689d5daff webdav: remove path from LocalFileSystem path errors
We don't want to show local paths in error messages.

`os.Is*` calls were replaced with `errors.Is` to also match the new
wrapped error.
2025-03-05 08:48:59 +01:00
Timo Furrer
002c347f47 caldav: support prop-filter in client requests
This change set adds support for correctly encoding the prop filters
into calendar requests.

Example Request with this change set:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<calendar-query
	xmlns="urn:ietf:params:xml:ns:caldav">
	<prop
		xmlns="DAV:">
		<calendar-data
			xmlns="urn:ietf:params:xml:ns:caldav">
			<comp
				xmlns="urn:ietf:params:xml:ns:caldav" name="VCALENDAR">
				<comp
					xmlns="urn:ietf:params:xml:ns:caldav" name="VEVENT">
					<prop
						xmlns="urn:ietf:params:xml:ns:caldav" name="UID">
					</prop>
					<prop
						xmlns="urn:ietf:params:xml:ns:caldav" name="ATTENDEE">
					</prop>
				</comp>
			</comp>
		</calendar-data>
		<getlastmodified
			xmlns="DAV:">
		</getlastmodified>
		<getetag
			xmlns="DAV:">
		</getetag>
	</prop>
	<filter
		xmlns="urn:ietf:params:xml:ns:caldav">
		<comp-filter
			xmlns="urn:ietf:params:xml:ns:caldav" name="VCALENDAR">
			<comp-filter
				xmlns="urn:ietf:params:xml:ns:caldav" name="VEVENT">
				<prop-filter
					xmlns="urn:ietf:params:xml:ns:caldav" name="UID">
					<text-match
						xmlns="urn:ietf:params:xml:ns:caldav">5bf5ee84-a9cf-4319-9def-437b00e2be8d
					</text-match>
				</prop-filter>
			</comp-filter>
		</comp-filter>
	</filter>
</calendar-query
```
2025-02-21 14:22:01 +01:00
Simon Ser
1b10baf554 ci: check gofmt, split build into separate task, enable -race 2025-02-21 10:52:48 +01:00
Timo Furrer
aba953c3b6 Implement context path discovery for CalDav/CardDav endpoint
This change set implements the "context path" discovery for the
CalDav/CardDav endpoints.

This basically implements the bootstrapping process as defined in
RFC6764 section 6, point 2 and 3.

What's missing in this implementation is the fallback that is described
in point 3, subpoint 3, which says that if the context path discovered
in the TXT RR is not reachable the .well-known URI should be used
instead.

I propose to implement this in a future iteration.
2025-02-21 10:50:48 +01:00
17 changed files with 1190 additions and 67 deletions

View File

@@ -4,6 +4,12 @@ packages:
sources:
- https://github.com/emersion/go-webdav
tasks:
- build: |
cd go-webdav
go build -race -v ./...
- test: |
cd go-webdav
go test -v ./...
go test -race -v ./...
- gofmt: |
cd go-webdav
test -z $(gofmt -l .)

View File

@@ -1,4 +1,6 @@
# go-webdav
Fork of go-webdav that supports ACL, synced with [original repo](https://github.com/emersion/go-webdav) (Implementation of ACL from [oliverpool/go-webdav](https://github.com/oliverpool/go-webdav))
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-webdav.svg)](https://pkg.go.dev/github.com/emersion/go-webdav)

View File

@@ -79,6 +79,12 @@ type CalendarCompRequest struct {
AllComps bool
Comps []CalendarCompRequest
Expand *CalendarExpandRequest
}
type CalendarExpandRequest struct {
Start, End time.Time
}
type CompFilter struct {

View File

@@ -154,7 +154,9 @@ func encodeCalendarReq(c *CalendarCompRequest) (*internal.Prop, error) {
return nil, err
}
calDataReq := calendarDataReq{Comp: compReq}
expandReq := encodeExpandRequest(c.Expand)
calDataReq := calendarDataReq{Comp: compReq, Expand: expandReq}
getLastModReq := internal.NewRawXMLElement(internal.GetLastModifiedName, nil, nil)
getETagReq := internal.NewRawXMLElement(internal.GetETagName, nil, nil)
@@ -172,6 +174,55 @@ func encodeCompFilter(filter *CompFilter) *compFilter {
for _, child := range filter.Comps {
encoded.CompFilters = append(encoded.CompFilters, *encodeCompFilter(&child))
}
for _, pf := range filter.Props {
encoded.PropFilters = append(encoded.PropFilters, *encodePropFilter(&pf))
}
return &encoded
}
func encodePropFilter(filter *PropFilter) *propFilter {
encoded := propFilter{Name: filter.Name}
if !filter.Start.IsZero() || !filter.End.IsZero() {
encoded.TimeRange = &timeRange{
Start: dateWithUTCTime(filter.Start),
End: dateWithUTCTime(filter.End),
}
}
encoded.TextMatch = encodeTextMatch(filter.TextMatch)
for _, pf := range filter.ParamFilter {
encoded.ParamFilter = append(encoded.ParamFilter, encodeParamFilter(pf))
}
return &encoded
}
func encodeParamFilter(pf ParamFilter) paramFilter {
encoded := paramFilter{
Name: pf.Name,
TextMatch: encodeTextMatch(pf.TextMatch),
}
return encoded
}
func encodeTextMatch(tm *TextMatch) *textMatch {
if tm == nil {
return nil
}
encoded := &textMatch{
Text: tm.Text,
NegateCondition: negateCondition(tm.NegateCondition),
}
return encoded
}
func encodeExpandRequest(e *CalendarExpandRequest) *expand {
if e == nil {
return nil
}
encoded := expand{
Start: dateWithUTCTime(e.Start),
End: dateWithUTCTime(e.End),
}
return &encoded
}

View File

@@ -179,7 +179,8 @@ func (t *dateWithUTCTime) MarshalText() ([]byte, error) {
type calendarDataReq struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"`
Comp *comp `xml:"comp,omitempty"`
// TODO: expand, limit-recurrence-set, limit-freebusy-set
Expand *expand `xml:"expand,omitempty"`
// TODO: limit-recurrence-set, limit-freebusy-set
}
// https://tools.ietf.org/html/rfc4791#section-9.6.1
@@ -194,6 +195,12 @@ type comp struct {
Comp []comp `xml:"comp,omitempty"`
}
type expand struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav expand"`
Start dateWithUTCTime `xml:"start,attr"`
End dateWithUTCTime `xml:"end,attr"`
}
// https://tools.ietf.org/html/rfc4791#section-9.6.4
type prop struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop"`

View File

@@ -366,7 +366,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
resType := b.resourceTypeAtPath(r.URL.Path)
var dataReq CalendarCompRequest
@@ -376,86 +376,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
case resourceTypeRoot:
resp, err := b.propFindRoot(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
case resourceTypeUserPrincipal:
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
if err != nil {
return nil, err
return err
}
if r.URL.Path == principalPath {
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
resp, err := b.propFindHomeSet(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth == internal.DepthInfinity {
resps_, err := b.propFindAllCalendars(r.Context(), propfind, true)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
}
} else {
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
return nil
}
case resourceTypeCalendarHomeSet:
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
if err != nil {
return nil, err
return err
}
if r.URL.Path == homeSetPath {
resp, err := b.propFindHomeSet(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
recurse := depth == internal.DepthInfinity
resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
} else {
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
return nil
}
case resourceTypeCalendar:
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
if err != nil {
return nil, err
return err
}
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
case resourceTypeCalendarObject:
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
if err != nil {
return nil, err
return err
}
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
}
return internal.NewMultiStatus(resps...), nil
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
}
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {

View File

@@ -233,3 +233,35 @@ func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
return nil, nil
}
func TestRedirections(t *testing.T) {
ctx := context.Background()
calendars := []Calendar{{Path: "/user/calendars/a"}}
ts := httptest.NewServer(&Handler{&testBackend{
calendars: calendars,
}, ""})
defer ts.Close()
client, err := NewClient(nil, ts.URL)
if err != nil {
t.Fatalf("error creating client: %s", err)
}
hsp, err := client.FindCalendarHomeSet(ctx, "/must-be-redirected/")
if err != nil {
t.Fatalf("error finding home set path: %s", err)
}
if want := "/user/calendars/"; hsp != want {
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
}
abs, err := client.FindCalendars(ctx, "/must-be-redirected/again/")
if err != nil {
t.Fatalf("error finding calendars: %s", err)
}
if len(abs) != 1 {
t.Fatalf("Found %d calendars, expected 1", len(abs))
}
if want := "/user/calendars/a"; abs[0].Path != want {
t.Fatalf("Found calendar at %s, expected %s", abs[0].Path, want)
}
}

View File

@@ -28,6 +28,7 @@ type AddressBook struct {
Description string
MaxResourceSize int64
SupportedAddressData []AddressDataType
ReadOnly bool
}
func (ab *AddressBook) SupportsAddressData(contentType, version string) bool {
@@ -107,6 +108,7 @@ type AddressObject struct {
ContentLength int64
ETag string
Card vcard.Card
ReadOnly bool
}
// SyncQuery is the query struct represents a sync-collection request

View File

@@ -194,6 +194,43 @@ func TestAddressBookDiscovery(t *testing.T) {
}
}
func TestRedirections(t *testing.T) {
ctx := context.Background()
h := Handler{&testBackend{}, ""}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, currentUserPrincipalKey, "/principal/")
ctx = context.WithValue(ctx, homeSetPathKey, "/principal/contacts/")
ctx = context.WithValue(ctx, addressBookPathKey, "/principal/contacts/default")
r = r.WithContext(ctx)
(&h).ServeHTTP(w, r)
}))
defer ts.Close()
client, err := NewClient(nil, ts.URL)
if err != nil {
t.Fatalf("error creating client: %s", err)
}
hsp, err := client.FindAddressBookHomeSet(ctx, "/must-be-redirected/")
if err != nil {
t.Fatalf("error finding home set path: %s", err)
}
if want := "/principal/contacts/"; hsp != want {
t.Fatalf("Found home set path '%s', expected '%s'", hsp, want)
}
abs, err := client.FindAddressBooks(ctx, "/must-be-redirected/again/")
if err != nil {
t.Fatalf("error finding address books: %s", err)
}
if len(abs) != 1 {
t.Fatalf("Found %d address books, expected 1", len(abs))
}
if want := "/principal/contacts/default"; abs[0].Path != want {
t.Fatalf("Found address book at %s, expected %s", abs[0].Path, want)
}
}
var mkcolRequestBody = `
<?xml version="1.0" encoding="utf-8" ?>
<D:mkcol xmlns:D="DAV:"

View File

@@ -332,7 +332,7 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
resType := b.resourceTypeAtPath(r.URL.Path)
var dataReq AddressDataRequest
@@ -342,86 +342,92 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
case resourceTypeRoot:
resp, err := b.propFindRoot(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
case resourceTypeUserPrincipal:
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
if err != nil {
return nil, err
return err
}
if r.URL.Path == principalPath {
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
resp, err := b.propFindHomeSet(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth == internal.DepthInfinity {
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, true)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
}
} else {
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect) // keep http method
return nil
}
case resourceTypeAddressBookHomeSet:
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
if err != nil {
return nil, err
return err
}
if r.URL.Path == homeSetPath {
resp, err := b.propFindHomeSet(r.Context(), propfind)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
recurse := depth == internal.DepthInfinity
resps_, err := b.propFindAllAddressBooks(r.Context(), propfind, recurse)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
} else {
http.Redirect(w, r, homeSetPath, http.StatusPermanentRedirect) // keep http method
return nil
}
case resourceTypeAddressBook:
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
if err != nil {
return nil, err
return err
}
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
if err != nil {
return nil, err
return err
}
resps = append(resps, resps_...)
}
case resourceTypeAddressObject:
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
if err != nil {
return nil, err
return err
}
resp, err := b.propFindAddressObject(r.Context(), propfind, ao)
if err != nil {
return nil, err
return err
}
resps = append(resps, *resp)
}
return internal.NewMultiStatus(resps...), nil
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
}
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
@@ -490,13 +496,23 @@ func (b *backend) propFindAddressBook(ctx context.Context, propfind *internal.Pr
}
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
},
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, addressBookName)),
supportedAddressDataName: internal.PropFindValue(&supportedAddressData{
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName, addressBookName), nil
},
supportedAddressDataName: func(*internal.RawXMLValue) (interface{}, error) {
return &supportedAddressData{
Types: []addressDataType{
{ContentType: vcard.MIMEType, Version: "3.0"},
{ContentType: vcard.MIMEType, Version: "4.0"},
},
}),
}, nil
},
internal.CurrentUserPrivilegeSetName: func(*internal.RawXMLValue) (interface{}, error) {
if ab.ReadOnly {
return &internal.CurrentUserPrivilegeSetReadOnly, nil
}
return &internal.CurrentUserPrivilegeSetReadWrite, nil
},
}
if ab.Name != "" {
@@ -563,6 +579,12 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
return &addressDataResp{Data: buf.Bytes()}, nil
},
internal.CurrentUserPrivilegeSetName: func(*internal.RawXMLValue) (interface{}, error) {
if ao.ReadOnly {
return &internal.CurrentUserPrivilegeSetReadOnly, nil
}
return &internal.CurrentUserPrivilegeSetReadWrite, nil
},
}
if ao.ContentLength > 0 {

View File

@@ -2,8 +2,10 @@ package webdav
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"os"
@@ -64,11 +66,17 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo {
}
func errFromOS(err error) error {
if os.IsNotExist(err) {
// Remove path from path errors so it's not returned to the user
var perr *fs.PathError
if errors.As(err, &perr) {
err = fmt.Errorf("%s: %w", perr.Op, perr.Err)
}
if errors.Is(err, fs.ErrNotExist) {
return NewHTTPError(http.StatusNotFound, err)
} else if os.IsPermission(err) {
} else if errors.Is(err, fs.ErrPermission) {
return NewHTTPError(http.StatusForbidden, err)
} else if os.IsTimeout(err) {
} else if errors.Is(err, os.ErrDeadlineExceeded) {
return NewHTTPError(http.StatusServiceUnavailable, err)
} else {
return err
@@ -95,9 +103,12 @@ func (fs LocalFileSystem) ReadDir(ctx context.Context, name string, recursive bo
var l []FileInfo
err = filepath.Walk(path, func(p string, fi os.FileInfo, err error) error {
if err != nil {
if err != nil && !errors.Is(err, os.ErrPermission) {
return err
}
if fi == nil {
return nil
}
href, err := fs.externalPath(p)
if err != nil {

548
internal/acl.go Normal file
View File

@@ -0,0 +1,548 @@
package internal
import (
"encoding/xml"
)
func NewPrivilege(name xml.Name) Privilege {
return Privilege{
Raw: NewRawXMLElement(name, nil, nil),
}
}
type Privilege struct {
XMLName xml.Name `xml:"DAV: privilege"`
Raw *RawXMLValue `xml:",any"`
}
func (p Privilege) Is(target xml.Name) bool {
got, ok := p.Raw.XMLName()
return ok && got == target
}
var (
/*
rfc3744#section-3.1
The read privilege controls methods that return information about the
state of the resource, including the resource's properties. Affected
methods include GET and PROPFIND. Any implementation-defined
privilege that also controls access to GET and PROPFIND must be
aggregated under DAV:read - if an ACL grants access to DAV:read, the
client may expect that no other privilege needs to be granted to have
access to GET and PROPFIND. Additionally, the read privilege MUST
control the OPTIONS method.
*/
Read = xml.Name{"DAV:", "read"}
/*
rfc3744#section-3.2
The write privilege controls methods that lock a resource or modify
the content, dead properties, or (in the case of a collection)
membership of the resource, such as PUT and PROPPATCH. Note that
state modification is also controlled via locking (see section 5.3 of
[RFC2518]), so effective write access requires that both write
privileges and write locking requirements are satisfied. Any
implementation-defined privilege that also controls access to methods
modifying content, dead properties or collection membership must be
aggregated under DAV:write, e.g., if an ACL grants access to
DAV:write, the client may expect that no other privilege needs to be
granted to have access to PUT and PROPPATCH.
*/
Write = xml.Name{"DAV:", "write"}
/*
rfc3744#section-3.3
The DAV:write-properties privilege controls methods that modify the
dead properties of the resource, such as PROPPATCH. Whether this
privilege may be used to control access to any live properties is
determined by the implementation. Any implementation-defined
privilege that also controls access to methods modifying dead
properties must be aggregated under DAV:write-properties - e.g., if
an ACL grants access to DAV:write-properties, the client can safely
expect that no other privilege needs to be granted to have access to
PROPPATCH.
*/
WriteProperties = xml.Name{"DAV:", "write-properties"}
/*
rfc3744#section-3.4
The DAV:write-content privilege controls methods that modify the
content of an existing resource, such as PUT. Any implementation-
defined privilege that also controls access to content must be
aggregated under DAV:write-content - e.g., if an ACL grants access to
DAV:write-content, the client can safely expect that no other
privilege needs to be granted to have access to PUT. Note that PUT -
when applied to an unmapped URI - creates a new resource and
therefore is controlled by the DAV:bind privilege on the parent
collection.
*/
WriteContent = xml.Name{"DAV:", "write-content"}
/*
rfc3744#section-3.5
The DAV:unlock privilege controls the use of the UNLOCK method by a
principal other than the lock owner (the principal that created a
lock can always perform an UNLOCK). While the set of users who may
lock a resource is most commonly the same set of users who may modify
a resource, servers may allow various kinds of administrators to
unlock resources locked by others. Any privilege controlling access
by non-lock owners to UNLOCK MUST be aggregated under DAV:unlock.
A lock owner can always remove a lock by issuing an UNLOCK with the
correct lock token and authentication credentials. That is, even if
a principal does not have DAV:unlock privilege, they can still remove
locks they own. Principals other than the lock owner can remove a
lock only if they have DAV:unlock privilege and they issue an UNLOCK
with the correct lock token. Lock timeout is not affected by the
DAV:unlock privilege.
*/
Unlock = xml.Name{"DAV:", "unlock"}
/*
rfc3744#section-3.6
The DAV:read-acl privilege controls the use of PROPFIND to retrieve
the DAV:acl property of the resource.
*/
ReadACL = xml.Name{"DAV:", "read-acl"}
/*
rfc3744#section-3.7
The DAV:read-current-user-privilege-set privilege controls the use of
PROPFIND to retrieve the DAV:current-user-privilege-set property of
the resource.
Clients are intended to use this property to visually indicate in
their UI items that are dependent on the permissions of a resource,
for example, by graying out resources that are not writable.
This privilege is separate from DAV:read-acl because there is a need
to allow most users access to the privileges permitted the current
user (due to its use in creating the UI), while the full ACL contains
information that may not be appropriate for the current authenticated
user. As a result, the set of users who can view the full ACL is
expected to be much smaller than those who can read the current user
privilege set, and hence distinct privileges are needed for each.
*/
ReadCurrentUserPrivilegeSet = xml.Name{"DAV:", "read-current-user-privilege-set"}
/*
rfc3744#section-3.8
The DAV:write-acl privilege controls use of the ACL method to modify
the DAV:acl property of the resource.
*/
WriteACL = xml.Name{"DAV:", "write-acl"}
/*
rfc3744#section-3.9
The DAV:bind privilege allows a method to add a new member URL to the
specified collection (for example via PUT or MKCOL). It is ignored
for resources that are not collections.
*/
Bind = xml.Name{"DAV:", "bind"}
/*
rfc3744#section-3.10
The DAV:unbind privilege allows a method to remove a member URL from
the specified collection (for example via DELETE or MOVE). It is
ignored for resources that are not collections.
*/
Unbind = xml.Name{"DAV:", "unbind"}
/*
rfc3744#section-3.11
DAV:all is an aggregate privilege that contains the entire set of
privileges that can be applied to the resource.
*/
All = xml.Name{"DAV:", "all"}
)
/*
rfc3744#section-4
rfc3744#section-5.5.1
The current user matches DAV:href only if that user is authenticated
as being (or being a member of) the principal identified by the URL
contained by that DAV:href.
The current user always matches DAV:all.
The current user matches DAV:authenticated only if authenticated.
The current user matches DAV:unauthenticated only if not
authenticated.
*/
type Principal struct {
XMLName xml.Name `xml:"DAV: principal"`
Raw *RawXMLValue `xml:",any"`
}
/*
rfc3744#section-4.1
This protected property, if non-empty, contains the URIs of network
resources with additional descriptive information about the
principal. This property identifies additional network resources
(i.e., it contains one or more URIs) that may be consulted by a
client to gain additional knowledge concerning a principal. One
expected use for this property is the storage of an LDAP [RFC2255]
scheme URL. A user-agent encountering an LDAP URL could use LDAP
[RFC2251] to retrieve additional machine-readable directory
information about the principal, and display that information in its
user interface. Support for this property is REQUIRED, and the value
is empty if no alternate URI exists for the principal.
*/
type AlternateURISet struct {
XMLName xml.Name `xml:"DAV: alternate-URI-set"`
Href []Href `xml:"href,omitempty"`
}
/*
rfc3744#section-4.2
A principal may have many URLs, but there must be one "principal URL"
that clients can use to uniquely identify a principal. This
protected property contains the URL that MUST be used to identify
this principal in an ACL request. Support for this property is
REQUIRED.
*/
type PrincipalURL struct {
XMLName xml.Name `xml:"DAV: principal-URL"`
Href Href `xml:"href,omitempty"`
}
/*
rfc3744#section-4.3
This property of a group principal identifies the principals that are
direct members of this group. Since a group may be a member of
another group, a group may also have indirect members (i.e., the
members of its direct members). A URL in the DAV:group-member-set
for a principal MUST be the DAV:principal-URL of that principal.
*/
type GroupMemberSet struct {
XMLName xml.Name `xml:"DAV: group-member-set"`
Href []Href `xml:"href,omitempty"`
}
/*
rfc3744#section-4.4
This protected property identifies the groups in which the principal
is directly a member. Note that a server may allow a group to be a
member of another group, in which case the DAV:group-membership of
those other groups would need to be queried in order to determine the
groups in which the principal is indirectly a member. Support for
this property is REQUIRED.
*/
type GroupMembership struct {
XMLName xml.Name `xml:"DAV: group-membership"`
Href []Href `xml:"href,omitempty"`
}
/*
rfc3744#section-5.1
This property identifies a particular principal as being the "owner"
of the resource. Since the owner of a resource often has special
access control capabilities (e.g., the owner frequently has permanent
DAV:write-acl privilege), clients might display the resource owner in
their user interface.
Servers MAY implement DAV:owner as protected property and MAY return
an empty DAV:owner element as property value in case no owner
information is available.
*/
type Owner struct {
XMLName xml.Name `xml:"DAV: owner"`
Href Href `xml:"href,omitempty"`
}
/*
rfc3744#section-5.2
This property identifies a particular principal as being the "group"
of the resource. This property is commonly found on repositories
that implement the Unix privileges model.
Servers MAY implement DAV:group as protected property and MAY return
an empty DAV:group element as property value in case no group
information is available.
*/
type Group struct {
XMLName xml.Name `xml:"DAV: group"`
Href Href `xml:"href,omitempty"`
}
/*
rfc3744#section-5.3
This is a protected property that identifies the privileges defined
for the resource.
*/
type SupportedPrivilegeSet struct {
XMLName xml.Name `xml:"DAV: supported-privilege-set"`
SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"`
}
/*
rfc3744#section-5.3
Each privilege appears as an XML element, where aggregate privileges
list as sub-elements all of the privileges that they aggregate.
*/
type SupportedPrivilege struct {
XMLName xml.Name `xml:"DAV: supported-privilege"`
Privilege Privilege `xml:"privilege"`
/*
Abstract will be nil if not set
rfc3744#section-5.3
An abstract privilege MUST NOT be used in an ACE for that resource.
Servers MUST fail an attempt to set an abstract privilege.
*/
Abstract *struct{} `xml:"abstract,omitempty"`
Description Description `xml:"description"`
SupportedPrivilege []SupportedPrivilege `xml:"supported-privilege"`
}
/*
rfc3744#section-5.3
A description is a human-readable description of what this privilege
controls access to. Servers MUST indicate the human language of the
description using the xml:lang attribute and SHOULD consider the HTTP
Accept-Language request header when selecting one of multiple
available languages.
*/
type Description struct {
XMLName xml.Name `xml:"DAV: description"`
Text string `xml:",chardata"`
Lang string `xml:"lang,attr,omitempty"`
}
/*
rfc3744#section-5.4
DAV:current-user-privilege-set is a protected property containing the
exact set of privileges (as computed by the server) granted to the
currently authenticated HTTP user. Aggregate privileges and their
contained privileges are listed. A user-agent can use the value of
this property to adjust its user interface to make actions
inaccessible (e.g., by graying out a menu item or button) for which
the current principal does not have permission. This property is
also useful for determining what operations the current principal can
perform, without having to actually execute an operation.
*/
type CurrentUserPrivilegeSet struct {
XMLName xml.Name `xml:"DAV: current-user-privilege-set"`
Privilege []Privilege `xml:"privilege"`
}
// convenience CurrentUserPrivilegeSet
var (
CurrentUserPrivilegeSetReadOnly = CurrentUserPrivilegeSet{
Privilege: []Privilege{NewPrivilege(Read)},
}
CurrentUserPrivilegeSetReadWrite = CurrentUserPrivilegeSet{
Privilege: []Privilege{NewPrivilege(Read), NewPrivilege(Write)},
}
)
/*
rfc3744#section-5.5
This is a protected property that specifies the list of access
control entries (ACEs), which define what principals are to get what
privileges for this resource.
*/
type ACL struct {
XMLName xml.Name `xml:"DAV: acl"`
ACE []ACE `xml:"ace"`
}
/*
rfc3744#section-5.5
Each DAV:ace element specifies the set of privileges to be either
granted or denied to a single principal. If the DAV:acl property is
empty, no principal is granted any privilege.
*/
type ACE struct {
XMLName xml.Name `xml:"DAV: ace"`
/*
rfc3744#section-5.5.1
The DAV:principal element identifies the principal to which this ACE
applies.
*/
Principal Principal `xml:"principal,omitempty"`
Grant *Grant `xml:"grant,omitempty"`
Deny *Deny `xml:"deny,omitempty"`
}
/*
rfc3744#section-5.5.2
Each DAV:grant or DAV:deny element specifies the set of privileges to
be either granted or denied to the specified principal. A DAV:grant
or DAV:deny element of the DAV:acl of a resource MUST only contain
non-abstract elements specified in the DAV:supported-privilege-set of
that resource.
*/
type Grant struct {
XMLName xml.Name `xml:"DAV: grant"`
Privilege []Privilege `xml:"privilege"`
}
/*
rfc3744#section-5.5.2
Each DAV:grant or DAV:deny element specifies the set of privileges to
be either granted or denied to the specified principal. A DAV:grant
or DAV:deny element of the DAV:acl of a resource MUST only contain
non-abstract elements specified in the DAV:supported-privilege-set of
that resource.
*/
type Deny struct {
XMLName xml.Name `xml:"DAV: deny"`
Privilege []Privilege `xml:"privilege"`
}
// to be continued (5.5.2. & following)
///////////////////////////////////////////////
var (
/*
rfc3744#section-5.6.1
This element indicates that ACEs with deny clauses are not allowed.
*/
GrantOnly = xml.Name{"DAV:", "grant-only"}
/*
rfc3744#section-5.6.2
This element indicates that ACEs with the <invert> element are not
allowed.
*/
NoInvert = xml.Name{"DAV:", "no-invert"}
/*
rfc3744#section-5.6.3
This element indicates that all deny ACEs must precede all grant
ACEs.
*/
DenyBeforeGrant = xml.Name{"DAV:", "deny-before-grant"}
)
var (
/*
rfc3744#section-8.1.1
The ACEs submitted in the ACL request MUST NOT
conflict with each other. This is a catchall error code indicating
that an implementation-specific ACL restriction has been violated.
*/
NoACEConflict = xml.Name{"DAV:", "no-ace-conflict"}
/*
rfc3744#section-8.1.1
The ACEs submitted in the ACL
request MUST NOT conflict with the protected ACEs on the resource.
For example, if the resource has a protected ACE granting DAV:write
to a given principal, then it would not be consistent if the ACL
request submitted an ACE denying DAV:write to the same principal.
*/
NoProtectedACEConflict = xml.Name{"DAV:", "no-protected-ace-conflict"}
/*
rfc3744#section-8.1.1
The ACEs submitted in the ACL
request MUST NOT conflict with the inherited ACEs on the resource.
For example, if the resource inherits an ACE from its parent
collection granting DAV:write to a given principal, then it would not
be consistent if the ACL request submitted an ACE denying DAV:write
to the same principal. Note that reporting of this error will be
implementation-dependent. Implementations MUST either report this
error or allow the ACE to be set, and then let normal ACE evaluation
rules determine whether the new ACE has any impact on the privileges
available to a specific principal.
*/
NoInheritedACEConflict = xml.Name{"DAV:", "no-inherited-ace-conflict"}
/*
rfc3744#section-8.1.1
The number of ACEs submitted in the ACL
request MUST NOT exceed the number of ACEs allowed on that resource.
However, ACL-compliant servers MUST support at least one ACE granting
privileges to a single principal, and one ACE granting privileges to
a group.
*/
LimitedNumberOfACEs = xml.Name{"DAV:", "limited-number-of-aces"}
// already defined above:
// DenyBeforeGrant
// GrantOnly
// NoInvert
/*
rfc3744#section-8.1.1
The ACL request MUST NOT attempt to grant or deny
an abstract privilege
*/
NoAbstract = xml.Name{"DAV:", "no-abstract"}
/*
rfc3744#section-8.1.1
The ACEs submitted in the ACL request
MUST be supported by the resource.
*/
NotSupportedPrivilege = xml.Name{"DAV:", "not-supported-privilege"}
/*
rfc3744#section-8.1.1
The result of the ACL request MUST
have at least one ACE for each principal identified in a
DAV:required-principal XML element in the ACL semantics of that
resource
*/
MissingRequiredPrincipal = xml.Name{"DAV:", "missing-required-principal"}
/*
rfc3744#section-8.1.1
Every principal URL in the ACL request
MUST identify a principal resource.
*/
RecognizedPrincipal = xml.Name{"DAV:", "recognized-principal"}
/*
rfc3744#section-8.1.1
The principals specified in the ACEs
submitted in the ACL request MUST be allowed as principals for the
resource. For example, a server where only authenticated principals
can access resources would not allow the DAV:all or
DAV:unauthenticated principals to be used in an ACE, since these
would allow unauthenticated access to resources.
*/
AllowedPrincipal = xml.Name{"DAV:", "allowed-principal"}
)

322
internal/acl_test.go Normal file
View File

@@ -0,0 +1,322 @@
package internal
import (
"encoding/xml"
"fmt"
"strings"
"testing"
)
func decodePropInsideMultiStatus(data []byte, v interface{}) error {
var ms MultiStatus
err := xml.Unmarshal(data, &ms)
if err != nil {
return err
}
if len(ms.Responses) != 1 {
return fmt.Errorf("expected 1 <response>, got %d", len(ms.Responses))
}
ps := ms.Responses[0].PropStats
if len(ps) != 1 {
return fmt.Errorf("expected 1 <propstat>, got %d", len(ps))
}
return ps[0].Prop.Decode(v)
}
func checkSupportedPrivilege(t *testing.T, sp SupportedPrivilege, privilege xml.Name, abstract bool, description string, children int) {
t.Helper()
if !sp.Privilege.Is(privilege) {
t.Errorf("expected %s, got %v", privilege, sp.Privilege.Raw)
}
if abstract {
if sp.Abstract == nil {
t.Errorf("missing expected <abstract>")
}
} else {
if sp.Abstract != nil {
t.Errorf("unexpected <abstract>")
}
}
if strings.TrimSpace(sp.Description.Text) != description {
t.Errorf("expected description %q, got %q", description, strings.TrimSpace(sp.Description.Text))
}
if strings.TrimSpace(sp.Description.Lang) != "en" {
t.Errorf("expected lang %q, got %q", "en", sp.Description.Lang)
}
if len(sp.SupportedPrivilege) != children {
t.Fatalf("expected %d <supported-privilege>, got %d", children, len(sp.SupportedPrivilege))
}
}
func TestACLMarshalling(t *testing.T) {
/* rfc3744#section-5.1.1 */
t.Run("owner", func(t *testing.T) {
var owner Owner
err := decodePropInsideMultiStatus([]byte(` <?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>http://www.example.com/papers/</D:href>
<D:propstat>
<D:prop>
<D:owner>
<D:href>http://www.example.com/acl/users/gstein</D:href>
</D:owner>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`), &owner)
if err != nil {
t.Error(err)
}
if owner.Href.String() != "http://www.example.com/acl/users/gstein" {
t.Fatalf("expected http://www.example.com/acl/users/gstein, got %s", owner.Href.String())
}
})
/* rfc3744#section-5.3.1 */
t.Run("supported-privilege-set", func(t *testing.T) {
var sps SupportedPrivilegeSet
err := decodePropInsideMultiStatus([]byte(` <?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>http://www.example.com/papers/</D:href>
<D:propstat>
<D:prop>
<D:supported-privilege-set>
<D:supported-privilege>
<D:privilege><D:all/></D:privilege>
<D:abstract/>
<D:description xml:lang="en">
Any operation
</D:description>
<D:supported-privilege>
<D:privilege><D:read/></D:privilege>
<D:description xml:lang="en">
Read any object
</D:description>
<D:supported-privilege>
<D:privilege><D:read-acl/></D:privilege>
<D:abstract/>
<D:description xml:lang="en">Read ACL</D:description>
</D:supported-privilege>
<D:supported-privilege>
<D:privilege>
<D:read-current-user-privilege-set/>
</D:privilege>
<D:abstract/>
<D:description xml:lang="en">
Read current user privilege set property
</D:description>
</D:supported-privilege>
</D:supported-privilege>
<D:supported-privilege>
<D:privilege><D:write/></D:privilege>
<D:description xml:lang="en">
Write any object
</D:description>
<D:supported-privilege>
<D:privilege><D:write-acl/></D:privilege>
<D:description xml:lang="en">
Write ACL
</D:description>
<D:abstract/>
</D:supported-privilege>
<D:supported-privilege>
<D:privilege><D:write-properties/></D:privilege>
<D:description xml:lang="en">
Write properties
</D:description>
</D:supported-privilege>
<D:supported-privilege>
<D:privilege><D:write-content/></D:privilege>
<D:description xml:lang="en">
Write resource content
</D:description>
</D:supported-privilege>
</D:supported-privilege>
<D:supported-privilege>
<D:privilege><D:unlock/></D:privilege>
<D:description xml:lang="en">
Unlock resource
</D:description>
</D:supported-privilege>
</D:supported-privilege>
</D:supported-privilege-set>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`), &sps)
if err != nil {
t.Error(err)
}
if len(sps.SupportedPrivilege) != 1 {
t.Fatalf("expected 1 <supported-privilege>, got %d", len(sps.SupportedPrivilege))
}
sp := sps.SupportedPrivilege[0]
checkSupportedPrivilege(t, sp, All, true, "Any operation", 3)
checkSupportedPrivilege(t, sp.SupportedPrivilege[0], Read, false, "Read any object", 2)
checkSupportedPrivilege(t, sp.SupportedPrivilege[1], Write, false, "Write any object", 3)
checkSupportedPrivilege(t, sp.SupportedPrivilege[2], Unlock, false, "Unlock resource", 0)
checkSupportedPrivilege(t, sp.SupportedPrivilege[0].SupportedPrivilege[0], ReadACL, true, "Read ACL", 0)
checkSupportedPrivilege(t, sp.SupportedPrivilege[0].SupportedPrivilege[1], ReadCurrentUserPrivilegeSet, true, "Read current user privilege set property", 0)
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[0], WriteACL, true, "Write ACL", 0)
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[1], WriteProperties, false, "Write properties", 0)
checkSupportedPrivilege(t, sp.SupportedPrivilege[1].SupportedPrivilege[2], WriteContent, false, "Write resource content", 0)
sp = SupportedPrivilege{
Privilege: NewPrivilege(All),
Abstract: &struct{}{},
Description: Description{Text: "all"},
}
buf, err := xml.Marshal(sp)
if err != nil {
t.Error(err)
}
if want := "<supported-privilege xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><all xmlns=\"DAV:\"></all></privilege><abstract></abstract><description xmlns=\"DAV:\">all</description></supported-privilege>"; string(buf) != want {
t.Errorf("expected %q, got %q", want, buf)
}
sp = SupportedPrivilege{
Privilege: NewPrivilege(Read),
Abstract: nil,
Description: Description{Text: "read"},
}
buf, err = xml.Marshal(sp)
if err != nil {
t.Error(err)
}
if want := "<supported-privilege xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><read xmlns=\"DAV:\"></read></privilege><description xmlns=\"DAV:\">read</description></supported-privilege>"; string(buf) != want {
t.Errorf("expected %q, got %q", want, buf)
}
})
/* rfc3744#section-5.4.1 */
t.Run("current-user-privilege-set", func(t *testing.T) {
var cups CurrentUserPrivilegeSet
err := decodePropInsideMultiStatus([]byte(`<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>http://www.example.com/papers/</D:href>
<D:propstat>
<D:prop>
<D:current-user-privilege-set>
<D:privilege><D:read/></D:privilege>
</D:current-user-privilege-set>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`), &cups)
if err != nil {
t.Error(err)
}
if len(cups.Privilege) != 1 {
t.Fatalf("expected 1 <privilege>, got %d", len(cups.Privilege))
}
if !cups.Privilege[0].Is(Read) {
t.Fatalf("expected <read>, got %v", cups.Privilege[0].Raw)
}
})
/* rfc3744#section-5.5.5 */
t.Run("acl", func(t *testing.T) {
var acl ACL
err := decodePropInsideMultiStatus([]byte(`<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>http://www.example.com/papers/</D:href>
<D:propstat>
<D:prop>
<D:acl>
<D:ace>
<D:principal>
<D:href
>http://www.example.com/acl/groups/maintainers</D:href>
</D:principal>
<D:grant>
<D:privilege><D:write/></D:privilege>
</D:grant>
</D:ace>
<D:ace>
<D:principal>
<D:all/>
</D:principal>
<D:grant>
<D:privilege><D:read/></D:privilege>
</D:grant>
</D:ace>
</D:acl>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`), &acl)
if err != nil {
t.Error(err)
}
if len(acl.ACE) != 2 {
t.Fatalf("expected 2 <ace>, got %d", len(acl.ACE))
}
{
ace := acl.ACE[0]
principalName, ok := ace.Principal.Raw.XMLName()
if want := (xml.Name{"DAV:", "href"}); !ok || principalName != want {
t.Fatalf("expected %s, got %s", want, principalName)
}
var href Href
err = ace.Principal.Raw.Decode(&href)
if err != nil {
t.Error(err)
}
if want := "http://www.example.com/acl/groups/maintainers"; href.String() != want {
t.Fatalf("expected %s, got %s", want, href.String())
}
if len(ace.Grant.Privilege) != 1 {
t.Fatalf("expected 1 <privilege>, got %d", len(ace.Grant.Privilege))
}
if !ace.Grant.Privilege[0].Is(Write) {
t.Fatalf("expected <write>, got %v", ace.Grant.Privilege[0].Raw)
}
}
{
ace := acl.ACE[1]
principalName, ok := ace.Principal.Raw.XMLName()
if want := (xml.Name{"DAV:", "all"}); !ok || principalName != want {
t.Fatalf("expected %s, got %s", want, principalName)
}
if len(ace.Grant.Privilege) != 1 {
t.Fatalf("expected 1 <privilege>, got %d", len(ace.Grant.Privilege))
}
if !ace.Grant.Privilege[0].Is(Read) {
t.Fatalf("expected <read>, got %v", ace.Grant.Privilege[0].Raw)
}
}
var ace ACE
ace.Principal.Raw = NewRawXMLElement(xml.Name{"DAV:", "authenticated"}, nil, nil)
ace.Grant = &Grant{
Privilege: []Privilege{
NewPrivilege(Read),
},
}
buf, err := xml.Marshal(ace)
if err != nil {
t.Error(err)
}
if want := "<ace xmlns=\"DAV:\"><principal xmlns=\"DAV:\"><authenticated xmlns=\"DAV:\"></authenticated></principal><grant xmlns=\"DAV:\"><privilege xmlns=\"DAV:\"><read xmlns=\"DAV:\"></read></privilege></grant></ace>"; string(buf) != want {
t.Errorf("expected %q, got %q", want, buf)
}
})
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"mime"
@@ -16,7 +17,9 @@ import (
)
// DiscoverContextURL performs a DNS-based CardDAV/CalDAV service discovery as
// described in RFC 6352 section 11. It returns the URL to the CardDAV server.
// described in RFC 6764. It returns the URL to the CardDAV/CalDAV server.
// Specifically it implements points 2 and 3 from the bootstrapping procedure
// defined in RFC 6764 section 6.
func DiscoverContextURL(ctx context.Context, service, domain string) (string, error) {
var resolver net.Resolver
@@ -31,23 +34,52 @@ func DiscoverContextURL(ctx context.Context, service, domain string) (string, er
}
if len(addrs) == 0 {
return "", fmt.Errorf("webdav: domain doesn't have an SRV record")
return "", errors.New("webdav: domain doesn't have an SRV record")
}
addr := addrs[0]
target := strings.TrimSuffix(addr.Target, ".")
if target == "" {
return "", fmt.Errorf("webdav: empty target in SRV record")
return "", errors.New("webdav: empty target in SRV record")
}
// TODO: perform a TXT lookup, check for a "path" key in the response
u := url.URL{Scheme: "https"}
txtName := fmt.Sprintf("_%ss._tcp.%s", service, domain)
txtRecords, err := resolver.LookupTXT(ctx, txtName)
if dnsErr, ok := err.(*net.DNSError); ok {
if dnsErr.IsTemporary {
return "", err
}
} else if err != nil {
return "", err
}
var path string
switch len(txtRecords) {
case 0:
path = "/.well-known/" + service
case 1:
record := txtRecords[0]
if !strings.HasPrefix(record, "path=") {
return "", fmt.Errorf("webdav: TXT record for %s does not contain the path key", txtName)
}
path = strings.TrimPrefix(record, "path=")
if path == "" {
return "", fmt.Errorf("webdav: empty path for %s TXT record", txtName)
}
default: // more than 1
return "", fmt.Errorf("webdav: more than one entry found on %s discovery TXT record", txtName)
}
u := url.URL{
Scheme: "https",
Path: path,
}
if addr.Port == 443 {
u.Host = target
} else {
u.Host = fmt.Sprintf("%v:%v", target, addr.Port)
}
u.Path = "/.well-known/" + service
return u.String(), nil
}

View File

@@ -22,6 +22,7 @@ var (
GetETagName = xml.Name{Namespace, "getetag"}
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
CurrentUserPrivilegeSetName = xml.Name{Namespace, "current-user-privilege-set"}
)
type Status struct {

View File

@@ -64,7 +64,7 @@ func ServeMultiStatus(w http.ResponseWriter, ms *MultiStatus) error {
type Backend interface {
Options(r *http.Request) (caps []string, allow []string, err error)
HeadGet(w http.ResponseWriter, r *http.Request) error
PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error)
PropFind(w http.ResponseWriter, r *http.Request, pf *PropFind, depth Depth) error
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
Put(w http.ResponseWriter, r *http.Request) error
Delete(r *http.Request) error
@@ -152,12 +152,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
}
}
ms, err := h.Backend.PropFind(r, &propfind, depth)
if err != nil {
return err
}
return ServeMultiStatus(w, ms)
return h.Backend.PropFind(w, r, &propfind, depth)
}
type PropFindFunc func(raw *RawXMLValue) (interface{}, error)

View File

@@ -115,39 +115,39 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
func (b *backend) PropFind(w http.ResponseWriter, r *http.Request, propfind *internal.PropFind, depth internal.Depth) error {
// TODO: use partial error Response on error
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if err != nil {
return nil, err
return err
}
var resps []internal.Response
if depth != internal.DepthZero && fi.IsDir {
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
if err != nil {
return nil, err
return err
}
resps = make([]internal.Response, len(children))
for i, child := range children {
resp, err := b.propFindFile(propfind, &child)
if err != nil {
return nil, err
return err
}
resps[i] = *resp
}
} else {
resp, err := b.propFindFile(propfind, fi)
if err != nil {
return nil, err
return err
}
resps = []internal.Response{*resp}
}
return internal.NewMultiStatus(resps...), nil
return internal.ServeMultiStatus(w, internal.NewMultiStatus(resps...))
}
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
@@ -189,8 +189,51 @@ func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*inte
}
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
// TODO: return a failed Response instead
return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported")
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if err != nil {
return nil, err
}
resp := &internal.Response{Hrefs: []internal.Href{internal.Href{Path: fi.Path}}}
for _, set := range update.Set {
for _, raw := range set.Prop.Raw {
xmlName, ok := raw.XMLName()
if !ok {
continue
}
emptyVal := internal.NewRawXMLElement(xmlName, nil, nil)
if err := resp.EncodeProp(http.StatusForbidden, emptyVal); err != nil {
return nil, err
}
}
}
for _, remove := range update.Remove {
for _, raw := range remove.Prop.Raw {
xmlName, ok := raw.XMLName()
if !ok {
continue
}
emptyVal := internal.NewRawXMLElement(xmlName, nil, nil)
if err := resp.EncodeProp(http.StatusForbidden, emptyVal); err != nil {
return nil, err
}
}
}
if len(resp.PropStats) == 0 {
return nil, internal.HTTPErrorf(http.StatusBadRequest,
"webdav: request missing properties to update")
}
return resp, nil
}
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {