mirror of
https://github.com/1f349/go-webdav.git
synced 2024-12-22 16:24:14 +00:00
Support custom user principal and home set paths
Currently, the user principal path and the home set path are both hardcoded to "/", for both CalDAV and CardDAV. This poses a challenge if one wishes to run a CardDAV and CalDAV server in the same server. This commit introduces the concept of a UserPrincipalBackend. This backend must provide the path of the current user's principal URL from the given request context. The CalDAV and CardDAV backends are extended to also function as UserPrincipalBackend. In addition, they are required to supply the path of the respective home set (`calendar-home-set` and `addressbook-home-set`). The CardDAV and CalDAV servers act accordingly. The individual servers will continue to work as before (including the option of keeping everything at "/"). If one wishes to run CardDAV and CalDAV in parallel, the new `webdav.ServeUserPrincipal()` can be used as a convenience function to serve a common user principal URL for both servers. The input for this function can be easily computed by the application by getting the home set paths from the backends and using `caldav.NewCalendarHomeSet()` and `carddav.NewAddressbookHomeSet()` to create the home sets. Note that the storage backend will have to know about these paths as well. For any non-trivial use case, a storage backend should probably have access to the same UserPrincipalBackend. That is, however, an implementation detail and doesn't have to be reflected in the interfaces.
This commit is contained in:
parent
b5c6f8927c
commit
6887b6b812
@ -7,8 +7,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-ical"
|
"github.com/emersion/go-ical"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewCalendarHomeSet(path string) webdav.BackendSuppliedHomeSet {
|
||||||
|
return &calendarHomeSet{Href: internal.Href{Path: path}}
|
||||||
|
}
|
||||||
|
|
||||||
type Calendar struct {
|
type Calendar struct {
|
||||||
Path string
|
Path string
|
||||||
Name string
|
Name string
|
||||||
|
@ -31,6 +31,10 @@ type calendarHomeSet struct {
|
|||||||
Href internal.Href `xml:"DAV: href"`
|
Href internal.Href `xml:"DAV: href"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *calendarHomeSet) GetXMLName() xml.Name {
|
||||||
|
return calendarHomeSetName
|
||||||
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc4791#section-5.2.1
|
// https://tools.ietf.org/html/rfc4791#section-5.2.1
|
||||||
type calendarDescription struct {
|
type calendarDescription struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-description"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-description"`
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-ical"
|
"github.com/emersion/go-ical"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
"github.com/emersion/go-webdav/internal"
|
"github.com/emersion/go-webdav/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,10 +16,13 @@ import (
|
|||||||
|
|
||||||
// Backend is a CalDAV server backend.
|
// Backend is a CalDAV server backend.
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
|
CalendarHomeSetPath(ctx context.Context) (string, error)
|
||||||
Calendar(ctx context.Context) (*Calendar, error)
|
Calendar(ctx context.Context) (*Calendar, error)
|
||||||
GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error)
|
GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error)
|
||||||
ListCalendarObjects(ctx context.Context, req *CalendarCompRequest) ([]CalendarObject, error)
|
ListCalendarObjects(ctx context.Context, req *CalendarCompRequest) ([]CalendarObject, error)
|
||||||
QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error)
|
QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error)
|
||||||
|
|
||||||
|
webdav.UserPrincipalBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler handles CalDAV HTTP requests. It can be used to create a CalDAV
|
// Handler handles CalDAV HTTP requests. It can be used to create a CalDAV
|
||||||
@ -35,12 +38,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Path == "/.well-known/caldav" {
|
principalPath, err := h.Backend.CurrentUserPrincipal(r.Context())
|
||||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
if err != nil {
|
||||||
|
http.Error(w, "caldav: failed to determine current user principal", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/.well-known/caldav" {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "REPORT":
|
case "REPORT":
|
||||||
err = h.handleReport(w, r)
|
err = h.handleReport(w, r)
|
||||||
@ -182,7 +190,17 @@ type backend struct {
|
|||||||
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
||||||
caps = []string{"calendar-access"}
|
caps = []string{"calendar-access"}
|
||||||
|
|
||||||
if r.URL.Path == "/" {
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/" || r.URL.Path == principalPath || r.URL.Path == homeSetPath {
|
||||||
return caps, []string{http.MethodOptions, "PROPFIND", "REPORT"}, nil
|
return caps, []string{http.MethodOptions, "PROPFIND", "REPORT"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,14 +227,30 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth internal.Depth) (*internal.Multistatus, error) {
|
func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth internal.Depth) (*internal.Multistatus, error) {
|
||||||
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var resps []internal.Response
|
var resps []internal.Response
|
||||||
if r.URL.Path == "/" {
|
|
||||||
|
if r.URL.Path == principalPath {
|
||||||
|
resp, err := b.propfindUserPrincipal(r.Context(), propfind, homeSetPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resps = append(resps, *resp)
|
||||||
|
} else if r.URL.Path == homeSetPath {
|
||||||
cal, err := b.Backend.Calendar(r.Context())
|
cal, err := b.Backend.Calendar(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propfindCalendar(propfind, cal)
|
resp, err := b.propfindCalendar(r.Context(), propfind, cal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -225,13 +259,38 @@ func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth i
|
|||||||
if depth != internal.DepthZero {
|
if depth != internal.DepthZero {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
return internal.NewMultistatus(resps...), nil
|
return internal.NewMultistatus(resps...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propfindCalendar(propfind *internal.Propfind, cal *Calendar) (*internal.Response, error) {
|
func (b *backend) propfindUserPrincipal(ctx context.Context, propfind *internal.Propfind, homeSetPath string) (*internal.Response, error) {
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
props := map[xml.Name]internal.PropfindFunc{
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||||
|
},
|
||||||
|
calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return internal.NewPropfindResponse(principalPath, propfind, props)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) propfindCalendar(ctx context.Context, propfind *internal.Propfind, cal *Calendar) (*internal.Response, error) {
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||||
|
},
|
||||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
return internal.NewResourceType(internal.CollectionName, calendarName), nil
|
return internal.NewResourceType(internal.CollectionName, calendarName), nil
|
||||||
},
|
},
|
||||||
@ -252,14 +311,6 @@ func (b *backend) propfindCalendar(propfind *internal.Propfind, cal *Calendar) (
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
// TODO: this is a principal property
|
|
||||||
calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
||||||
return &calendarHomeSet{Href: internal.Href{Path: "/"}}, nil
|
|
||||||
},
|
|
||||||
// TODO: this should be set on all resources
|
|
||||||
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
||||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: "/"}}, nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cal.Description != "" {
|
if cal.Description != "" {
|
||||||
|
@ -7,8 +7,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewAddressBookHomeSet(path string) webdav.BackendSuppliedHomeSet {
|
||||||
|
return &addressbookHomeSet{Href: internal.Href{Path: path}}
|
||||||
|
}
|
||||||
|
|
||||||
type AddressDataType struct {
|
type AddressDataType struct {
|
||||||
ContentType string
|
ContentType string
|
||||||
Version string
|
Version string
|
||||||
|
@ -29,6 +29,10 @@ type addressbookHomeSet struct {
|
|||||||
Href internal.Href `xml:"DAV: href"`
|
Href internal.Href `xml:"DAV: href"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *addressbookHomeSet) GetXMLName() xml.Name {
|
||||||
|
return addressBookHomeSetName
|
||||||
|
}
|
||||||
|
|
||||||
type addressbookDescription struct {
|
type addressbookDescription struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-description"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-description"`
|
||||||
Description string `xml:",chardata"`
|
Description string `xml:",chardata"`
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
"github.com/emersion/go-webdav/internal"
|
"github.com/emersion/go-webdav/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,12 +26,15 @@ type PutAddressObjectOptions struct {
|
|||||||
|
|
||||||
// Backend is a CardDAV server backend.
|
// Backend is a CardDAV server backend.
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
|
AddressbookHomeSetPath(ctx context.Context) (string, error)
|
||||||
AddressBook(ctx context.Context) (*AddressBook, error)
|
AddressBook(ctx context.Context) (*AddressBook, error)
|
||||||
GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error)
|
GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error)
|
||||||
ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error)
|
ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error)
|
||||||
QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error)
|
QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error)
|
||||||
PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error)
|
PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error)
|
||||||
DeleteAddressObject(ctx context.Context, path string) error
|
DeleteAddressObject(ctx context.Context, path string) error
|
||||||
|
|
||||||
|
webdav.UserPrincipalBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler handles CardDAV HTTP requests. It can be used to create a CardDAV
|
// Handler handles CardDAV HTTP requests. It can be used to create a CardDAV
|
||||||
@ -46,12 +50,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Path == "/.well-known/carddav" {
|
principalPath, err := h.Backend.CurrentUserPrincipal(r.Context())
|
||||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
if err != nil {
|
||||||
|
http.Error(w, "carddav: failed to determine current user principal", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/.well-known/carddav" {
|
||||||
|
http.Redirect(w, r, principalPath, http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "REPORT":
|
case "REPORT":
|
||||||
err = h.handleReport(w, r)
|
err = h.handleReport(w, r)
|
||||||
@ -176,7 +185,7 @@ func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query
|
|||||||
AllProp: query.AllProp,
|
AllProp: query.AllProp,
|
||||||
PropName: query.PropName,
|
PropName: query.PropName,
|
||||||
}
|
}
|
||||||
resp, err := b.propfindAddressObject(&propfind, &ao)
|
resp, err := b.propfindAddressObject(ctx, &propfind, &ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -216,7 +225,7 @@ func (h *Handler) handleMultiget(ctx context.Context, w http.ResponseWriter, mul
|
|||||||
AllProp: multiget.AllProp,
|
AllProp: multiget.AllProp,
|
||||||
PropName: multiget.PropName,
|
PropName: multiget.PropName,
|
||||||
}
|
}
|
||||||
resp, err := b.propfindAddressObject(&propfind, ao)
|
resp, err := b.propfindAddressObject(ctx, &propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -234,7 +243,17 @@ type backend struct {
|
|||||||
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
||||||
caps = []string{"addressbook"}
|
caps = []string{"addressbook"}
|
||||||
|
|
||||||
if r.URL.Path == "/" {
|
homeSetPath, err := b.Backend.AddressbookHomeSetPath(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/" || r.URL.Path == principalPath || r.URL.Path == homeSetPath {
|
||||||
// Note: some clients assume the address book is read-only when
|
// Note: some clients assume the address book is read-only when
|
||||||
// DELETE/MKCOL are missing
|
// DELETE/MKCOL are missing
|
||||||
return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil
|
return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil
|
||||||
@ -259,10 +278,6 @@ func (b *backend) Options(r *http.Request) (caps []string, allow []string, err e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
||||||
if r.URL.Path == "/" {
|
|
||||||
return &internal.HTTPError{Code: http.StatusMethodNotAllowed}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataReq AddressDataRequest
|
var dataReq AddressDataRequest
|
||||||
if r.Method != http.MethodHead {
|
if r.Method != http.MethodHead {
|
||||||
dataReq.AllProp = true
|
dataReq.AllProp = true
|
||||||
@ -287,16 +302,32 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth internal.Depth) (*internal.Multistatus, error) {
|
func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth internal.Depth) (*internal.Multistatus, error) {
|
||||||
|
homeSetPath, err := b.Backend.AddressbookHomeSetPath(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var dataReq AddressDataRequest
|
var dataReq AddressDataRequest
|
||||||
|
|
||||||
var resps []internal.Response
|
var resps []internal.Response
|
||||||
if r.URL.Path == "/" {
|
|
||||||
|
if r.URL.Path == principalPath {
|
||||||
|
resp, err := b.propfindUserPrincipal(r.Context(), propfind, homeSetPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resps = append(resps, *resp)
|
||||||
|
} else if r.URL.Path == homeSetPath {
|
||||||
ab, err := b.Backend.AddressBook(r.Context())
|
ab, err := b.Backend.AddressBook(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propfindAddressBook(propfind, ab)
|
resp, err := b.propfindAddressBook(r.Context(), propfind, ab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -309,7 +340,7 @@ func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth i
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ao := range aos {
|
for _, ao := range aos {
|
||||||
resp, err := b.propfindAddressObject(propfind, &ao)
|
resp, err := b.propfindAddressObject(r.Context(), propfind, &ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -322,7 +353,7 @@ func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth i
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.propfindAddressObject(propfind, ao)
|
resp, err := b.propfindAddressObject(r.Context(), propfind, ao)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -332,8 +363,31 @@ func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth i
|
|||||||
return internal.NewMultistatus(resps...), nil
|
return internal.NewMultistatus(resps...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propfindAddressBook(propfind *internal.Propfind, ab *AddressBook) (*internal.Response, error) {
|
func (b *backend) propfindUserPrincipal(ctx context.Context, propfind *internal.Propfind, homeSetPath string) (*internal.Response, error) {
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
props := map[xml.Name]internal.PropfindFunc{
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||||
|
},
|
||||||
|
addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &addressbookHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return internal.NewPropfindResponse(principalPath, propfind, props)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) propfindAddressBook(ctx context.Context, propfind *internal.Propfind, ab *AddressBook) (*internal.Response, error) {
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||||
|
},
|
||||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
return internal.NewResourceType(internal.CollectionName, addressBookName), nil
|
return internal.NewResourceType(internal.CollectionName, addressBookName), nil
|
||||||
},
|
},
|
||||||
@ -351,14 +405,6 @@ func (b *backend) propfindAddressBook(propfind *internal.Propfind, ab *AddressBo
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
// TODO: this is a principal property
|
|
||||||
addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
||||||
return &addressbookHomeSet{Href: internal.Href{Path: "/"}}, nil
|
|
||||||
},
|
|
||||||
// TODO: this should be set on all resources
|
|
||||||
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
||||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: "/"}}, nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ab.MaxResourceSize > 0 {
|
if ab.MaxResourceSize > 0 {
|
||||||
@ -370,8 +416,15 @@ func (b *backend) propfindAddressBook(propfind *internal.Propfind, ab *AddressBo
|
|||||||
return internal.NewPropfindResponse(ab.Path, propfind, props)
|
return internal.NewPropfindResponse(ab.Path, propfind, props)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) propfindAddressObject(propfind *internal.Propfind, ao *AddressObject) (*internal.Response, error) {
|
func (b *backend) propfindAddressObject(ctx context.Context, propfind *internal.Propfind, ao *AddressObject) (*internal.Response, error) {
|
||||||
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
props := map[xml.Name]internal.PropfindFunc{
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||||
|
},
|
||||||
internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
return &internal.GetContentType{Type: vcard.MIMEType}, nil
|
return &internal.GetContentType{Type: vcard.MIMEType}, nil
|
||||||
},
|
},
|
||||||
|
32
elements.go
Normal file
32
elements.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/emersion/go-webdav/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
principalName = xml.Name{"DAV:", "principal"}
|
||||||
|
principalAlternateURISetName = xml.Name{"DAV:", "alternate-URI-set"}
|
||||||
|
principalURLName = xml.Name{"DAV:", "principal-URL"}
|
||||||
|
groupMembershipName = xml.Name{"DAV:", "group-membership"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3744#section-4.1
|
||||||
|
type principalAlternateURISet struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: alternate-URI-set"`
|
||||||
|
Hrefs []internal.Href `xml:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3744#section-4.2
|
||||||
|
type principalURL struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: principal-URL"`
|
||||||
|
Href internal.Href `xml:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3744#section-4.4
|
||||||
|
type groupMembership struct {
|
||||||
|
XMLName xml.Name `xml:"DAV: group-membership"`
|
||||||
|
Hrefs []internal.Href `xml:"href"`
|
||||||
|
}
|
72
server.go
72
server.go
@ -1,11 +1,13 @@
|
|||||||
package webdav
|
package webdav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/emersion/go-webdav/internal"
|
"github.com/emersion/go-webdav/internal"
|
||||||
)
|
)
|
||||||
@ -243,3 +245,73 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr
|
|||||||
}
|
}
|
||||||
return created, err
|
return created, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a
|
||||||
|
// CardDAV addressbook-home-set. It should only be created via
|
||||||
|
// `caldav.NewCalendarHomeSet()` or `carddav.NewAddressbookHomeSet()`. Only to
|
||||||
|
// be used server-side, for listing a user's home sets as determined by the
|
||||||
|
// (external) backend.
|
||||||
|
type BackendSuppliedHomeSet interface {
|
||||||
|
GetXMLName() xml.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPrincipalBackend can determine the current user's principal URL for a
|
||||||
|
// given request context.
|
||||||
|
type UserPrincipalBackend interface {
|
||||||
|
CurrentUserPrincipal(ctx context.Context) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServeUserPrincipalOptions struct {
|
||||||
|
UserPrincipalPath string
|
||||||
|
HomeSets []BackendSuppliedHomeSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeUserPrincipal replies to requests for the user principal URL
|
||||||
|
func ServeUserPrincipal(w http.ResponseWriter, r *http.Request, options *ServeUserPrincipalOptions) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodOptions:
|
||||||
|
caps := []string{"1", "3"}
|
||||||
|
allow := []string{http.MethodOptions, "PROPFIND"}
|
||||||
|
w.Header().Add("DAV", strings.Join(caps, ", "))
|
||||||
|
w.Header().Add("Allow", strings.Join(allow, ", "))
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
case "PROPFIND":
|
||||||
|
if err := serveUserPrincipalPropfind(w, r, options); err != nil {
|
||||||
|
internal.ServeError(w, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveUserPrincipalPropfind(w http.ResponseWriter, r *http.Request, options *ServeUserPrincipalOptions) error {
|
||||||
|
var propfind internal.Propfind
|
||||||
|
if err := internal.DecodeXMLRequest(r, &propfind); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
props := map[xml.Name]internal.PropfindFunc{
|
||||||
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return internal.NewResourceType(principalName), nil
|
||||||
|
},
|
||||||
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: options.UserPrincipalPath}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle Depth and more properties
|
||||||
|
|
||||||
|
for _, homeSet := range options.HomeSets {
|
||||||
|
hs := homeSet // capture variable for closure
|
||||||
|
props[homeSet.GetXMLName()] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||||
|
return hs, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := internal.NewPropfindResponse(r.URL.Path, &propfind, props)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ms := internal.NewMultistatus(*resp)
|
||||||
|
return internal.ServeMultistatus(w, ms)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user