go.net/webdav: new Handler, FileSystem, LockSystem and lockInfo types.
LGTM=dave
R=nmvc, dave
CC=bradfitz, dr.volker.dobler, golang-codereviews
https://golang.org/cl/169240043
diff --git a/webdav/file.go b/webdav/file.go
new file mode 100644
index 0000000..a2953ae
--- /dev/null
+++ b/webdav/file.go
@@ -0,0 +1,28 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package webdav
+
+import (
+ "io"
+ "net/http"
+ "os"
+)
+
+// TODO: comment that paths are always "/"-separated, even for Windows servers.
+
+type FileSystem interface {
+ Mkdir(path string, perm os.FileMode) error
+ OpenFile(path string, flag int, perm os.FileMode) (File, error)
+ RemoveAll(path string) error
+ Stat(path string) (os.FileInfo, error)
+}
+
+type File interface {
+ http.File
+ io.Writer
+}
+
+// TODO: a MemFS implementation.
+// TODO: a RealFS implementation, backed by the real, OS-provided file system.
diff --git a/webdav/if.go b/webdav/if.go
index e4d4670..416e81c 100644
--- a/webdav/if.go
+++ b/webdav/if.go
@@ -16,18 +16,10 @@
lists []ifList
}
-// ifList is a conjunction (AND) of ifConditions, and an optional resource tag.
+// ifList is a conjunction (AND) of Conditions, and an optional resource tag.
type ifList struct {
resourceTag string
- conditions []ifCondition
-}
-
-// ifCondition can match a WebDAV resource, based on a stateToken or ETag.
-// Exactly one of stateToken and entityTag should be non-empty.
-type ifCondition struct {
- not bool
- stateToken string
- entityTag string
+ conditions []Condition
}
// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string
@@ -110,19 +102,19 @@
}
}
-func parseCondition(s string) (c ifCondition, remaining string, ok bool) {
+func parseCondition(s string) (c Condition, remaining string, ok bool) {
tokenType, tokenStr, s := lex(s)
if tokenType == notTokenType {
- c.not = true
+ c.Not = true
tokenType, tokenStr, s = lex(s)
}
switch tokenType {
case strTokenType, angleTokenType:
- c.stateToken = tokenStr
+ c.Token = tokenStr
case squareTokenType:
- c.entityTag = tokenStr
+ c.ETag = tokenStr
default:
- return ifCondition{}, "", false
+ return Condition{}, "", false
}
return c, s, true
}
diff --git a/webdav/if_test.go b/webdav/if_test.go
index 684e8b3..aad61a4 100644
--- a/webdav/if_test.go
+++ b/webdav/if_test.go
@@ -38,7 +38,7 @@
`<foo>`,
ifHeader{},
}, {
- "bad: no list after resource #1",
+ "bad: no list after resource #2",
`<foo> <bar> (a)`,
ifHeader{},
}, {
@@ -66,12 +66,12 @@
`(Not Not a)`,
ifHeader{},
}, {
- "good: one list with a stateToken",
+ "good: one list with a Token",
`(a)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `a`,
+ conditions: []Condition{{
+ Token: `a`,
}},
}},
},
@@ -80,8 +80,8 @@
`([a])`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- entityTag: `a`,
+ conditions: []Condition{{
+ ETag: `a`,
}},
}},
},
@@ -90,15 +90,15 @@
`(Not a Not b Not [d])`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- not: true,
- stateToken: `a`,
+ conditions: []Condition{{
+ Not: true,
+ Token: `a`,
}, {
- not: true,
- stateToken: `b`,
+ Not: true,
+ Token: `b`,
}, {
- not: true,
- entityTag: `d`,
+ Not: true,
+ ETag: `d`,
}},
}},
},
@@ -107,12 +107,12 @@
`(a) (b)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `a`,
+ conditions: []Condition{{
+ Token: `a`,
}},
}, {
- conditions: []ifCondition{{
- stateToken: `b`,
+ conditions: []Condition{{
+ Token: `b`,
}},
}},
},
@@ -121,14 +121,14 @@
`(Not a) (Not b)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- not: true,
- stateToken: `a`,
+ conditions: []Condition{{
+ Not: true,
+ Token: `a`,
}},
}, {
- conditions: []ifCondition{{
- not: true,
- stateToken: `b`,
+ conditions: []Condition{{
+ Not: true,
+ Token: `b`,
}},
}},
},
@@ -139,8 +139,8 @@
ifHeader{
lists: []ifList{{
resourceTag: `http://www.example.com/users/f/fielding/index.html`,
- conditions: []ifCondition{{
- stateToken: `urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6`,
+ conditions: []Condition{{
+ Token: `urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6`,
}},
}},
},
@@ -149,8 +149,8 @@
`(<urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf>)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
+ conditions: []Condition{{
+ Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
}},
}},
},
@@ -161,8 +161,8 @@
ifHeader{
lists: []ifList{{
resourceTag: `http://example.com/locked/`,
- conditions: []ifCondition{{
- stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
+ conditions: []Condition{{
+ Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
}},
}},
},
@@ -173,8 +173,8 @@
ifHeader{
lists: []ifList{{
resourceTag: `http://example.com/locked/member`,
- conditions: []ifCondition{{
- stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
+ conditions: []Condition{{
+ Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`,
}},
}},
},
@@ -184,12 +184,12 @@
(<urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77>)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4`,
+ conditions: []Condition{{
+ Token: `urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4`,
}},
}, {
- conditions: []ifCondition{{
- stateToken: `urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77`,
+ conditions: []Condition{{
+ Token: `urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77`,
}},
}},
},
@@ -198,8 +198,8 @@
`(<urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4>)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4`,
+ conditions: []Condition{{
+ Token: `urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4`,
}},
}},
},
@@ -210,14 +210,14 @@
(["I am another ETag"])`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
+ conditions: []Condition{{
+ Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
}, {
- entityTag: `"I am an ETag"`,
+ ETag: `"I am an ETag"`,
}},
}, {
- conditions: []ifCondition{{
- entityTag: `"I am another ETag"`,
+ conditions: []Condition{{
+ ETag: `"I am another ETag"`,
}},
}},
},
@@ -227,11 +227,11 @@
<urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- not: true,
- stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
+ conditions: []Condition{{
+ Not: true,
+ Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
}, {
- stateToken: `urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092`,
+ Token: `urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092`,
}},
}},
},
@@ -241,13 +241,13 @@
(Not <DAV:no-lock>)`,
ifHeader{
lists: []ifList{{
- conditions: []ifCondition{{
- stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
+ conditions: []Condition{{
+ Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
}},
}, {
- conditions: []ifCondition{{
- not: true,
- stateToken: `DAV:no-lock`,
+ conditions: []Condition{{
+ Not: true,
+ Token: `DAV:no-lock`,
}},
}},
},
@@ -259,15 +259,15 @@
ifHeader{
lists: []ifList{{
resourceTag: `/resource1`,
- conditions: []ifCondition{{
- stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
+ conditions: []Condition{{
+ Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
}, {
- entityTag: `W/"A weak ETag"`,
+ ETag: `W/"A weak ETag"`,
}},
}, {
resourceTag: `/resource1`,
- conditions: []ifCondition{{
- entityTag: `"strong ETag"`,
+ conditions: []Condition{{
+ ETag: `"strong ETag"`,
}},
}},
},
@@ -278,8 +278,8 @@
ifHeader{
lists: []ifList{{
resourceTag: `http://www.example.com/specs/`,
- conditions: []ifCondition{{
- stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
+ conditions: []Condition{{
+ Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`,
}},
}},
},
@@ -289,8 +289,8 @@
ifHeader{
lists: []ifList{{
resourceTag: `/specs/rfc2518.doc`,
- conditions: []ifCondition{{
- entityTag: `"4217"`,
+ conditions: []Condition{{
+ ETag: `"4217"`,
}},
}},
},
@@ -300,9 +300,9 @@
ifHeader{
lists: []ifList{{
resourceTag: `/specs/rfc2518.doc`,
- conditions: []ifCondition{{
- not: true,
- entityTag: `"4217"`,
+ conditions: []Condition{{
+ Not: true,
+ ETag: `"4217"`,
}},
}},
},
diff --git a/webdav/lock.go b/webdav/lock.go
new file mode 100644
index 0000000..6538ded
--- /dev/null
+++ b/webdav/lock.go
@@ -0,0 +1,44 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package webdav
+
+import (
+ "errors"
+ "io"
+ "time"
+)
+
+var (
+ ErrConfirmationFailed = errors.New("webdav: confirmation failed")
+ ErrForbidden = errors.New("webdav: forbidden")
+ ErrNoSuchLock = errors.New("webdav: no such lock")
+)
+
+// Condition can match a WebDAV resource, based on a token or ETag.
+// Exactly one of Token and ETag should be non-empty.
+type Condition struct {
+ Not bool
+ Token string
+ ETag string
+}
+
+type LockSystem interface {
+ // TODO: comment that the conditions should be ANDed together.
+ Confirm(path string, conditions ...Condition) (c io.Closer, err error)
+ // TODO: comment that token should be an absolute URI as defined by RFC 3986,
+ // Section 4.3. In particular, it should not contain whitespace.
+ Create(path string, now time.Time, ld LockDetails) (token string, c io.Closer, err error)
+ Refresh(token string, now time.Time, duration time.Duration) (ld LockDetails, c io.Closer, err error)
+ Unlock(token string) error
+}
+
+type LockDetails struct {
+ Depth int // Negative means infinite depth.
+ Duration time.Duration // Negative means unlimited duration.
+ OwnerXML string // Verbatim XML.
+ Path string
+}
+
+// TODO: a MemLS implementation.
diff --git a/webdav/webdav.go b/webdav/webdav.go
new file mode 100644
index 0000000..7ffb859
--- /dev/null
+++ b/webdav/webdav.go
@@ -0,0 +1,295 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package webdav etc etc TODO.
+package webdav
+
+// TODO: ETag, properties.
+// TODO: figure out what/when is responsible for path cleaning: no "../../etc/passwd"s.
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "os"
+ "time"
+)
+
+// TODO: define the PropSystem interface.
+type PropSystem interface{}
+
+type Handler struct {
+ // FileSystem is the virtual file system.
+ FileSystem FileSystem
+ // LockSystem is the lock management system.
+ LockSystem LockSystem
+ // PropSystem is an optional property management system. If non-nil, TODO.
+ PropSystem PropSystem
+ // Logger is an optional error logger. If non-nil, it will be called
+ // whenever handling a http.Request results in an error.
+ Logger func(*http.Request, error)
+}
+
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ status, err := http.StatusBadRequest, error(nil)
+ if h.FileSystem == nil {
+ status, err = http.StatusInternalServerError, errNoFileSystem
+ } else if h.LockSystem == nil {
+ status, err = http.StatusInternalServerError, errNoLockSystem
+ } else {
+ // TODO: COPY, MOVE, PROPFIND, PROPPATCH methods. Also, OPTIONS??
+ switch r.Method {
+ case "GET", "HEAD", "POST":
+ status, err = h.handleGetHeadPost(w, r)
+ case "DELETE":
+ status, err = h.handleDelete(w, r)
+ case "PUT":
+ status, err = h.handlePut(w, r)
+ case "MKCOL":
+ status, err = h.handleMkcol(w, r)
+ case "LOCK":
+ status, err = h.handleLock(w, r)
+ case "UNLOCK":
+ status, err = h.handleUnlock(w, r)
+ }
+ }
+
+ if status != 0 {
+ w.WriteHeader(status)
+ if status != http.StatusNoContent {
+ w.Write([]byte(StatusText(status)))
+ }
+ }
+ if h.Logger != nil && err != nil {
+ h.Logger(r, err)
+ }
+}
+
+func (h *Handler) confirmLocks(r *http.Request) (closer io.Closer, status int, err error) {
+ ih, ok := parseIfHeader(r.Header.Get("If"))
+ if !ok {
+ return nil, http.StatusBadRequest, errInvalidIfHeader
+ }
+ // ih is a disjunction (OR) of ifLists, so any ifList will do.
+ for _, l := range ih.lists {
+ path := l.resourceTag
+ if path == "" {
+ path = r.URL.Path
+ }
+ closer, err = h.LockSystem.Confirm(path, l.conditions...)
+ if err == ErrConfirmationFailed {
+ continue
+ }
+ if err != nil {
+ return nil, http.StatusInternalServerError, err
+ }
+ return closer, 0, nil
+ }
+ return nil, http.StatusPreconditionFailed, errLocked
+}
+
+func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
+ // TODO: check locks for read-only access??
+ f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDONLY, 0)
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+ defer f.Close()
+ fi, err := f.Stat()
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+ http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f)
+ return 0, nil
+}
+
+func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
+ closer, status, err := h.confirmLocks(r)
+ if err != nil {
+ return status, err
+ }
+ defer closer.Close()
+
+ if err := h.FileSystem.RemoveAll(r.URL.Path); err != nil {
+ // TODO: MultiStatus.
+ return http.StatusMethodNotAllowed, err
+ }
+ return http.StatusNoContent, nil
+}
+
+func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
+ closer, status, err := h.confirmLocks(r)
+ if err != nil {
+ return status, err
+ }
+ defer closer.Close()
+
+ f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+ if err != nil {
+ return http.StatusNotFound, err
+ }
+ defer f.Close()
+ if _, err := io.Copy(f, r.Body); err != nil {
+ return http.StatusMethodNotAllowed, err
+ }
+ return http.StatusCreated, nil
+}
+
+func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
+ closer, status, err := h.confirmLocks(r)
+ if err != nil {
+ return status, err
+ }
+ defer closer.Close()
+
+ if err := h.FileSystem.Mkdir(r.URL.Path, 0777); err != nil {
+ if os.IsNotExist(err) {
+ return http.StatusConflict, err
+ }
+ return http.StatusMethodNotAllowed, err
+ }
+ return http.StatusCreated, nil
+}
+
+func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
+ duration, err := parseTimeout(r.Header.Get("Timeout"))
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ li, status, err := readLockInfo(r.Body)
+ if err != nil {
+ return status, err
+ }
+
+ token, ld := "", LockDetails{}
+ if li == (lockInfo{}) {
+ // An empty lockInfo means to refresh the lock.
+ ih, ok := parseIfHeader(r.Header.Get("If"))
+ if !ok {
+ return http.StatusBadRequest, errInvalidIfHeader
+ }
+ if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
+ token = ih.lists[0].conditions[0].Token
+ }
+ if token == "" {
+ return http.StatusBadRequest, errInvalidLockToken
+ }
+ var closer io.Closer
+ ld, closer, err = h.LockSystem.Refresh(token, time.Now(), duration)
+ if err != nil {
+ if err == ErrNoSuchLock {
+ return http.StatusPreconditionFailed, err
+ }
+ return http.StatusInternalServerError, err
+ }
+ defer closer.Close()
+
+ } else {
+ depth, err := parseDepth(r.Header.Get("Depth"))
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ ld = LockDetails{
+ Depth: depth,
+ Duration: duration,
+ OwnerXML: li.Owner.InnerXML,
+ Path: r.URL.Path,
+ }
+ var closer io.Closer
+ token, closer, err = h.LockSystem.Create(r.URL.Path, time.Now(), ld)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ defer func() {
+ if retErr != nil {
+ h.LockSystem.Unlock(token)
+ }
+ }()
+ defer closer.Close()
+
+ // Create the resource if it didn't previously exist.
+ if _, err := h.FileSystem.Stat(r.URL.Path); err != nil {
+ f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+ if err != nil {
+ // TODO: detect missing intermediate dirs and return http.StatusConflict?
+ return http.StatusInternalServerError, err
+ }
+ f.Close()
+ w.WriteHeader(http.StatusCreated)
+ // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
+ // Lock-Token value is a Coded-URL. We add angle brackets.
+ w.Header().Set("Lock-Token", "<"+token+">")
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/xml; charset=utf-8")
+ writeLockInfo(w, token, ld)
+ return 0, nil
+}
+
+func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {
+ // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
+ // Lock-Token value is a Coded-URL. We strip its angle brackets.
+ t := r.Header.Get("Lock-Token")
+ if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
+ return http.StatusBadRequest, errInvalidLockToken
+ }
+ t = t[1 : len(t)-1]
+
+ switch err = h.LockSystem.Unlock(t); err {
+ case nil:
+ return http.StatusNoContent, err
+ case ErrForbidden:
+ return http.StatusForbidden, err
+ case ErrNoSuchLock:
+ return http.StatusConflict, err
+ default:
+ return http.StatusInternalServerError, err
+ }
+}
+
+func parseDepth(s string) (int, error) {
+ // TODO: implement.
+ return -1, nil
+}
+
+func parseTimeout(s string) (time.Duration, error) {
+ // TODO: implement.
+ return 1 * time.Second, nil
+}
+
+// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
+const (
+ StatusMulti = 207
+ StatusUnprocessableEntity = 422
+ StatusLocked = 423
+ StatusFailedDependency = 424
+ StatusInsufficientStorage = 507
+)
+
+func StatusText(code int) string {
+ switch code {
+ case StatusMulti:
+ return "Multi-Status"
+ case StatusUnprocessableEntity:
+ return "Unprocessable Entity"
+ case StatusLocked:
+ return "Locked"
+ case StatusFailedDependency:
+ return "Failed Dependency"
+ case StatusInsufficientStorage:
+ return "Insufficient Storage"
+ }
+ return http.StatusText(code)
+}
+
+var (
+ errInvalidIfHeader = errors.New("webdav: invalid If header")
+ errInvalidLockInfo = errors.New("webdav: invalid lock info")
+ errInvalidLockToken = errors.New("webdav: invalid lock token")
+ errLocked = errors.New("webdav: locked")
+ errNoFileSystem = errors.New("webdav: no file system")
+ errNoLockSystem = errors.New("webdav: no lock system")
+ errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
+)
diff --git a/webdav/xml.go b/webdav/xml.go
new file mode 100644
index 0000000..5939373
--- /dev/null
+++ b/webdav/xml.go
@@ -0,0 +1,96 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package webdav
+
+// The XML encoding is covered by Section 14.
+// http://www.webdav.org/specs/rfc4918.html#xml.element.definitions
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
+type lockInfo struct {
+ XMLName xml.Name `xml:"lockinfo"`
+ Exclusive *struct{} `xml:"lockscope>exclusive"`
+ Shared *struct{} `xml:"lockscope>shared"`
+ Write *struct{} `xml:"locktype>write"`
+ Owner owner `xml:"owner"`
+}
+
+// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
+type owner struct {
+ InnerXML string `xml:",innerxml"`
+}
+
+func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
+ c := &countingReader{r: r}
+ if err = xml.NewDecoder(c).Decode(&li); err != nil {
+ if err == io.EOF {
+ if c.n == 0 {
+ // An empty body means to refresh the lock.
+ // http://www.webdav.org/specs/rfc4918.html#refreshing-locks
+ return lockInfo{}, 0, nil
+ }
+ err = errInvalidLockInfo
+ }
+ return lockInfo{}, http.StatusBadRequest, err
+ }
+ // We only support exclusive (non-shared) write locks. In practice, these are
+ // the only types of locks that seem to matter.
+ if li.Exclusive == nil || li.Shared != nil || li.Write == nil {
+ return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo
+ }
+ return li, 0, nil
+}
+
+type countingReader struct {
+ n int
+ r io.Reader
+}
+
+func (c *countingReader) Read(p []byte) (int, error) {
+ n, err := c.r.Read(p)
+ c.n += n
+ return n, err
+}
+
+func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
+ depth := "infinity"
+ if d := ld.Depth; d >= 0 {
+ depth = strconv.Itoa(d)
+ }
+ timeout := ld.Duration / time.Second
+ return fmt.Fprintf(w, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
+ "<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>\n"+
+ " <D:locktype><D:write/></D:locktype>\n"+
+ " <D:lockscope><D:exclusive/></D:lockscope>\n"+
+ " <D:depth>%s</D:depth>\n"+
+ " <D:owner>%s</D:owner>\n"+
+ " <D:timeout>Second-%d</D:timeout>\n"+
+ " <D:locktoken><D:href>%s</D:href></D:locktoken>\n"+
+ " <D:lockroot><D:href>%s</D:href></D:lockroot>\n"+
+ "</D:activelock></D:lockdiscovery></D:prop>",
+ depth, ld.OwnerXML, timeout, escape(token), escape(ld.Path),
+ )
+}
+
+func escape(s string) string {
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '"', '&', '\'', '<', '>':
+ b := bytes.NewBuffer(nil)
+ xml.EscapeText(b, []byte(s))
+ return b.String()
+ }
+ }
+ return s
+}
diff --git a/webdav/xml_test.go b/webdav/xml_test.go
new file mode 100644
index 0000000..26149a2
--- /dev/null
+++ b/webdav/xml_test.go
@@ -0,0 +1,129 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package webdav
+
+import (
+ "encoding/xml"
+ "net/http"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestParseLockInfo(t *testing.T) {
+ // The "section x.y.z" test cases come from section x.y.z of the spec at
+ // http://www.webdav.org/specs/rfc4918.html
+ testCases := []struct {
+ desc string
+ input string
+ wantLI lockInfo
+ wantStatus int
+ }{{
+ "bad: junk",
+ "xxx",
+ lockInfo{},
+ http.StatusBadRequest,
+ }, {
+ "bad: invalid owner XML",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>\n" +
+ " <D:href> no end tag \n" +
+ " </D:owner>\n" +
+ "</D:lockinfo>",
+ lockInfo{},
+ http.StatusBadRequest,
+ }, {
+ "bad: invalid UTF-8",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>\n" +
+ " <D:href> \xff </D:href>\n" +
+ " </D:owner>\n" +
+ "</D:lockinfo>",
+ lockInfo{},
+ http.StatusBadRequest,
+ }, {
+ "bad: unfinished XML #1",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n",
+ lockInfo{},
+ http.StatusBadRequest,
+ }, {
+ "bad: unfinished XML #2",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>\n",
+ lockInfo{},
+ http.StatusBadRequest,
+ }, {
+ "good: empty",
+ "",
+ lockInfo{},
+ 0,
+ }, {
+ "good: plain-text owner",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>gopher</D:owner>\n" +
+ "</D:lockinfo>",
+ lockInfo{
+ XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
+ Exclusive: new(struct{}),
+ Write: new(struct{}),
+ Owner: owner{
+ InnerXML: "gopher",
+ },
+ },
+ 0,
+ }, {
+ "section 9.10.7",
+ "" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:exclusive/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>\n" +
+ " <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
+ " </D:owner>\n" +
+ "</D:lockinfo>",
+ lockInfo{
+ XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
+ Exclusive: new(struct{}),
+ Write: new(struct{}),
+ Owner: owner{
+ InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ",
+ },
+ },
+ 0,
+ }}
+
+ for _, tc := range testCases {
+ li, status, err := readLockInfo(strings.NewReader(tc.input))
+ if tc.wantStatus != 0 {
+ if err == nil {
+ t.Errorf("%s: got nil error, want non-nil", tc.desc)
+ continue
+ }
+ } else if err != nil {
+ t.Errorf("%s: %v", tc.desc, err)
+ continue
+ }
+ if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
+ t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
+ tc.desc, li, status, tc.wantLI, tc.wantStatus)
+ continue
+ }
+ }
+}