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
This commit is contained in:
Nigel Tao
2014-11-11 17:46:57 +11:00
parent ef3d74d079
commit baf9fd4387
7 changed files with 662 additions and 78 deletions

28
webdav/file.go Normal file
View File

@@ -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.

View File

@@ -16,18 +16,10 @@ type ifHeader struct {
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 parseList(s string) (l ifList, remaining string, ok bool) {
}
}
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
}

View File

@@ -38,7 +38,7 @@ func TestParseIfHeader(t *testing.T) {
`<foo>`,
ifHeader{},
}, {
"bad: no list after resource #1",
"bad: no list after resource #2",
`<foo> <bar> (a)`,
ifHeader{},
}, {
@@ -66,12 +66,12 @@ func TestParseIfHeader(t *testing.T) {
`(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 @@ func TestParseIfHeader(t *testing.T) {
`([a])`,
ifHeader{
lists: []ifList{{
conditions: []ifCondition{{
entityTag: `a`,
conditions: []Condition{{
ETag: `a`,
}},
}},
},
@@ -90,15 +90,15 @@ func TestParseIfHeader(t *testing.T) {
`(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 @@ func TestParseIfHeader(t *testing.T) {
`(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 @@ func TestParseIfHeader(t *testing.T) {
`(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 @@ func TestParseIfHeader(t *testing.T) {
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 @@ func TestParseIfHeader(t *testing.T) {
`(<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 @@ func TestParseIfHeader(t *testing.T) {
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 @@ func TestParseIfHeader(t *testing.T) {
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 @@ func TestParseIfHeader(t *testing.T) {
(<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 @@ func TestParseIfHeader(t *testing.T) {
`(<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 @@ func TestParseIfHeader(t *testing.T) {
(["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 @@ func TestParseIfHeader(t *testing.T) {
<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 @@ func TestParseIfHeader(t *testing.T) {
(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 @@ func TestParseIfHeader(t *testing.T) {
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 @@ func TestParseIfHeader(t *testing.T) {
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 @@ func TestParseIfHeader(t *testing.T) {
ifHeader{
lists: []ifList{{
resourceTag: `/specs/rfc2518.doc`,
conditions: []ifCondition{{
entityTag: `"4217"`,
conditions: []Condition{{
ETag: `"4217"`,
}},
}},
},
@@ -300,9 +300,9 @@ func TestParseIfHeader(t *testing.T) {
ifHeader{
lists: []ifList{{
resourceTag: `/specs/rfc2518.doc`,
conditions: []ifCondition{{
not: true,
entityTag: `"4217"`,
conditions: []Condition{{
Not: true,
ETag: `"4217"`,
}},
}},
},

44
webdav/lock.go Normal file
View File

@@ -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.

295
webdav/webdav.go Normal file
View File

@@ -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")
)

96
webdav/xml.go Normal file
View File

@@ -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
}

129
webdav/xml_test.go Normal file
View File

@@ -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
}
}
}