Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions caldav/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,14 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr
return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Move not implemented")
}

func (b *backend) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (lock *internal.Lock, created bool, err error) {
return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "caldav: unsupported method")
}

func (b *backend) Unlock(r *http.Request, tokenHref string) error {
return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
}

// https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1
type PreconditionType string

Expand Down
9 changes: 9 additions & 0 deletions carddav/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path"
"strconv"
"strings"
"time"

"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav"
Expand Down Expand Up @@ -733,6 +734,14 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr
return false, internal.HTTPErrorf(http.StatusNotImplemented, "carddav: Move not implemented")
}

func (b *backend) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (lock *internal.Lock, created bool, err error) {
return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "carddav: unsupported method")
}

func (b *backend) Unlock(r *http.Request, tokenHref string) error {
return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
}

// PreconditionType as defined in https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1
type PreconditionType string

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.13
require (
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
github.com/google/uuid v1.6.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
71 changes: 71 additions & 0 deletions internal/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var (
GetContentTypeName = xml.Name{Namespace, "getcontenttype"}
GetLastModifiedName = xml.Name{Namespace, "getlastmodified"}
GetETagName = xml.Name{Namespace, "getetag"}
SupportedLockName = xml.Name{Namespace, "supportedlock"}

CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
)
Expand Down Expand Up @@ -346,6 +347,19 @@ type GetContentType struct {
Type string `xml:",chardata"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-15.10
type SupportedLock struct {
XMLName xml.Name `xml:"DAV: supportedlock"`
LockEntries []LockEntry `xml:"lockentry"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.10
type LockEntry struct {
XMLName xml.Name `xml:"DAV: lockentry"`
LockScope LockScope `xml:"lockscope"`
LockType LockType `xml:"locktype"`
}

type Time time.Time

func (t *Time) UnmarshalText(b []byte) error {
Expand Down Expand Up @@ -450,3 +464,60 @@ type Limit struct {
XMLName xml.Name `xml:"DAV: limit"`
NResults uint `xml:"nresults"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.11
type LockInfo struct {
XMLName xml.Name `xml:"DAV: lockinfo"`
LockScope LockScope `xml:"lockscope"`
LockType LockType `xml:"locktype"`
Owner *Owner `xml:"owner,omitempty"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.13
type LockScope struct {
XMLName xml.Name `xml:"DAV: lockscope"`
Exclusive *struct{} `xml:"exclusive"`
Shared *struct{} `xml:"shared"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.15
type LockType struct {
XMLName xml.Name `xml:"DAV: locktype"`
Write *struct{} `xml:"write"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.17
type Owner struct {
XMLName xml.Name `xml:"DAV: owner"`
// TODO
}

// https://www.rfc-editor.org/rfc/rfc4918#section-15.8
type LockDiscovery struct {
XMLName xml.Name `xml:"DAV: lockdiscovery"`
ActiveLock []ActiveLock `xml:"activelock,omitempty"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.1
type ActiveLock struct {
XMLName xml.Name `xml:"DAV: activelock"`
LockScope LockScope `xml:"lockscope"`
LockType LockType `xml:"locktype"`
Depth Depth `xml:"depth"`
Owner *Owner `xml:"owner,omitempty"`
Timeout *Timeout `xml:"timeout,omitempty"`
LockToken *LockToken `xml:"locktoken,omitempty"`
LockRoot LockRoot `xml:"lockroot"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.14
type LockToken struct {
XMLName xml.Name `xml:"DAV: locktoken"`
Href string `xml:"href"`
}

// https://www.rfc-editor.org/rfc/rfc4918#section-14.12
type LockRoot struct {
XMLName xml.Name `xml:"DAV: lockroot"`
Href string `xml:"href"`
}
208 changes: 208 additions & 0 deletions internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)

// Depth indicates whether a request applies to the resource's members. It's
Expand Down Expand Up @@ -48,6 +51,10 @@ func (d Depth) String() string {
panic("webdav: invalid Depth value")
}

func (d Depth) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}

// ParseOverwrite parses an Overwrite header.
func ParseOverwrite(s string) (bool, error) {
switch s {
Expand All @@ -68,6 +75,207 @@ func FormatOverwrite(overwrite bool) string {
}
}

type Timeout struct {
Duration time.Duration
}

func ParseTimeout(s string) (Timeout, error) {
if s == "Infinite" {
return Timeout{}, nil
} else if strings.HasPrefix(s, "Second-") {
n, err := strconv.Atoi(strings.TrimPrefix(s, "Second-"))
if err != nil || n <= 0 {
return Timeout{}, fmt.Errorf("webdav: invalid Timeout value")
}
return Timeout{Duration: time.Duration(n) * time.Second}, nil
} else {
return Timeout{}, fmt.Errorf("webdav: invalid Timeout value")
}
}

func (t Timeout) String() string {
if t.Duration == 0 {
return "Infinite"
}
return fmt.Sprintf("Second-%d", t.Duration/time.Second)
}

func (t Timeout) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}

func ParseLockToken(s string) (string, error) {
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
return "", fmt.Errorf("webdav: invalid Lock-Token value")
}
return s[1 : len(s)-1], nil
}

func FormatLockToken(token string) string {
return fmt.Sprintf("<%v>", token)
}

func ParseSubmittedToken(h http.Header) (string, error) {
hif := h.Get("If")
if hif == "" {
return "", nil
}

conditions, err := ParseConditions(hif)
if err != nil {
return "", &HTTPError{http.StatusBadRequest, err}
}

if len(conditions) == 0 {
return "", nil
}
if len(conditions) > 1 {
return "", HTTPErrorf(http.StatusBadRequest, "webdav: multiple lists are not supported in the If header field")
}
if len(conditions[0]) == 0 {
return "", nil
}
if len(conditions[0]) > 1 {
return "", HTTPErrorf(http.StatusBadRequest, "webdav: multiple conditions are not supported in the If header field")
}

return conditions[0][0].Token, nil
}

// Condition is a condition to match lock tokens and entity tags.
//
// Only one of Token or ETag is set.
type Condition struct {
Resource string
Not bool
Token string
ETag string
}

type conditionParser struct {
s string
}

func (p *conditionParser) acceptByte(ch byte) bool {
if len(p.s) == 0 || p.s[0] != ch {
return false
}
p.s = p.s[1:]
return true
}

func (p *conditionParser) expectByte(ch byte) error {
if len(p.s) == 0 {
return fmt.Errorf("webdav: invalid If value: expected %q, got EOF", ch)
} else if p.s[0] != ch {
return fmt.Errorf("webdav: invalid If value: expected %q, got %q", ch, p.s[0])
}
p.s = p.s[1:]
return nil
}

func (p *conditionParser) lws() bool {
lws := false
for p.acceptByte(' ') || p.acceptByte('\t') {
lws = true
}
return lws
}

func (p *conditionParser) consumeUntilByte(ch byte) (string, error) {
i := strings.IndexByte(p.s, ch)
if i < 0 {
return "", fmt.Errorf("webdav: invalid If value: expected %q, got EOF", ch)
}
s := p.s[:i]
p.s = p.s[i+1:]
return s, nil
}

func (p *conditionParser) condition() (*Condition, error) {
not := strings.HasPrefix(p.s, "Not")
if not {
p.s = strings.TrimPrefix(p.s, "Not")
p.lws()
}

if p.acceptByte('<') {
token, err := p.consumeUntilByte('>')
if err != nil {
return nil, err
}
return &Condition{Not: not, Token: token}, nil
} else if p.acceptByte('[') {
etag, err := p.consumeUntilByte(']')
if err != nil {
return nil, err
}
return &Condition{Not: not, ETag: etag}, nil
} else {
return nil, fmt.Errorf("webdav: invalid If value: invalid condition")
}
}

func (p *conditionParser) list() ([]Condition, error) {
if err := p.expectByte('('); err != nil {
return nil, err
}
p.lws()

var l []Condition
for !p.acceptByte(')') {
cond, err := p.condition()
if err != nil {
return nil, err
}
l = append(l, *cond)
p.lws()
}

return l, nil
}

func (p *conditionParser) parse() ([][]Condition, error) {
var conditions [][]Condition
for {
p.lws()
if p.s == "" {
break
}

var resource string
if p.acceptByte('<') {
var err error
resource, err = p.consumeUntilByte('>')
if err != nil {
return nil, err
}
p.lws()
}

l, err := p.list()
if err != nil {
return nil, err
}

for i := range l {
l[i].Resource = resource
}

conditions = append(conditions, l)
}

if len(conditions) == 0 {
return nil, fmt.Errorf("webdav: invalid If value: empty list")
}
return conditions, nil
}

func ParseConditions(s string) ([][]Condition, error) {
p := conditionParser{s}
return p.parse()
}

type HTTPError struct {
Code int
Err error
Expand Down
Loading