mirror of
https://github.com/golang/net.git
synced 2026-04-01 02:47:08 +09:00
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:
28
webdav/file.go
Normal file
28
webdav/file.go
Normal 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.
|
||||
22
webdav/if.go
22
webdav/if.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
44
webdav/lock.go
Normal 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
295
webdav/webdav.go
Normal 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
96
webdav/xml.go
Normal 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
129
webdav/xml_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user