blob: 8a8f6dc1ce7544f1344e9b8397ee768e22cbb0b9 [file] [log] [blame]
// 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 // import "golang.org/x/net/webdav"
// TODO: ETag, properties.
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")
)