go-webdav/server.go

326 lines
9.6 KiB
Go
Raw Normal View History

package webdav
import (
2022-03-21 08:16:50 +00:00
"context"
"encoding/xml"
"io"
"net/http"
"os"
"strconv"
2022-03-21 08:16:50 +00:00
"strings"
"github.com/emersion/go-webdav/internal"
)
2020-01-21 20:01:18 +00:00
// FileSystem is a WebDAV server backend.
type FileSystem interface {
2023-12-19 20:29:28 +00:00
Open(ctx context.Context, name string) (io.ReadCloser, error)
Stat(ctx context.Context, name string) (*FileInfo, error)
ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error)
Create(ctx context.Context, name string) (io.WriteCloser, error)
RemoveAll(ctx context.Context, name string) error
Mkdir(ctx context.Context, name string) error
Copy(ctx context.Context, name, dest string, recursive, overwrite bool) (created bool, err error)
MoveAll(ctx context.Context, name, dest string, overwrite bool) (created bool, err error)
}
2020-01-21 20:01:18 +00:00
// Handler handles WebDAV HTTP requests. It can be used to create a WebDAV
// server.
type Handler struct {
FileSystem FileSystem
}
2020-01-21 20:01:18 +00:00
// ServeHTTP implements http.Handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.FileSystem == nil {
2020-01-17 10:30:42 +00:00
http.Error(w, "webdav: no filesystem available", http.StatusInternalServerError)
return
}
2020-01-17 10:30:42 +00:00
b := backend{h.FileSystem}
hh := internal.Handler{&b}
hh.ServeHTTP(w, r)
}
// NewHTTPError creates a new error that is associated with an HTTP status code
// and optionally an error that lead to it. Backends can use this functions to
// return errors that convey some semantics (e.g. 404 not found, 403 access
// denied, etc) while also providing an (optional) arbitrary error context
// (intended for humans).
func NewHTTPError(statusCode int, cause error) error {
return &internal.HTTPError{Code: statusCode, Err: cause}
}
2020-01-17 10:30:42 +00:00
type backend struct {
FileSystem FileSystem
}
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
2023-12-19 20:29:28 +00:00
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
2020-01-17 10:41:44 +00:00
if os.IsNotExist(err) {
return nil, []string{http.MethodOptions, http.MethodPut, "MKCOL"}, nil
2020-01-17 10:41:44 +00:00
} else if err != nil {
return nil, nil, err
2020-01-17 10:41:44 +00:00
}
allow = []string{
http.MethodOptions,
http.MethodDelete,
"PROPFIND",
"COPY",
"MOVE",
}
if !fi.IsDir {
allow = append(allow, http.MethodHead, http.MethodGet, http.MethodPut)
2020-01-17 10:41:44 +00:00
}
return nil, allow, nil
2020-01-17 10:41:44 +00:00
}
2020-01-17 10:30:42 +00:00
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
2023-12-19 20:29:28 +00:00
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
} else if err != nil {
return err
}
if fi.IsDir {
return &internal.HTTPError{Code: http.StatusMethodNotAllowed}
}
2023-12-19 20:29:28 +00:00
f, err := b.FileSystem.Open(r.Context(), r.URL.Path)
if err != nil {
return err
}
defer f.Close()
2020-01-17 10:32:13 +00:00
w.Header().Set("Content-Length", strconv.FormatInt(fi.Size, 10))
2020-01-21 21:43:13 +00:00
if fi.MIMEType != "" {
w.Header().Set("Content-Type", fi.MIMEType)
}
if !fi.ModTime.IsZero() {
w.Header().Set("Last-Modified", fi.ModTime.UTC().Format(http.TimeFormat))
}
if fi.ETag != "" {
2020-02-03 20:48:31 +00:00
w.Header().Set("ETag", internal.ETag(fi.ETag).String())
}
2020-01-21 21:43:13 +00:00
if rs, ok := f.(io.ReadSeeker); ok {
// If it's an io.Seeker, use http.ServeContent which supports ranges
http.ServeContent(w, r, r.URL.Path, fi.ModTime, rs)
} else {
if r.Method != http.MethodHead {
io.Copy(w, f)
}
}
return nil
}
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
// TODO: use partial error Response on error
2023-12-19 20:29:28 +00:00
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if os.IsNotExist(err) {
return nil, &internal.HTTPError{Code: http.StatusNotFound, Err: err}
} else if err != nil {
2020-01-17 10:30:42 +00:00
return nil, err
}
2020-01-15 17:39:25 +00:00
var resps []internal.Response
if depth != internal.DepthZero && fi.IsDir {
2023-12-19 20:29:28 +00:00
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
2020-01-15 17:39:25 +00:00
if err != nil {
return nil, err
2020-01-15 17:39:25 +00:00
}
resps = make([]internal.Response, len(children))
for i, child := range children {
resp, err := b.propFindFile(propfind, &child)
if err != nil {
return nil, err
2020-01-15 17:39:25 +00:00
}
resps[i] = *resp
2020-01-15 17:39:25 +00:00
}
} else {
resp, err := b.propFindFile(propfind, fi)
if err != nil {
return nil, err
}
resps = []internal.Response{*resp}
2020-01-15 17:39:25 +00:00
}
return internal.NewMultiStatus(resps...), nil
2020-01-15 17:39:25 +00:00
}
func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*internal.Response, error) {
props := make(map[xml.Name]internal.PropFindFunc)
2020-01-17 16:09:23 +00:00
props[internal.ResourceTypeName] = func(*internal.RawXMLValue) (interface{}, error) {
var types []xml.Name
if fi.IsDir {
types = append(types, internal.CollectionName)
}
return internal.NewResourceType(types...), nil
}
if !fi.IsDir {
2020-01-17 16:09:23 +00:00
props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
return &internal.GetContentLength{Length: fi.Size}, nil
2020-01-15 18:08:38 +00:00
}
2020-01-21 21:44:10 +00:00
if !fi.ModTime.IsZero() {
props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
return &internal.GetLastModified{LastModified: internal.Time(fi.ModTime)}, nil
}
2020-01-15 18:08:38 +00:00
}
2020-01-21 21:43:13 +00:00
if fi.MIMEType != "" {
props[internal.GetContentTypeName] = func(*internal.RawXMLValue) (interface{}, error) {
return &internal.GetContentType{Type: fi.MIMEType}, nil
}
}
if fi.ETag != "" {
props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
2020-02-03 20:48:31 +00:00
return &internal.GetETag{ETag: internal.ETag(fi.ETag)}, nil
}
}
}
return internal.NewPropFindResponse(fi.Path, propfind, props)
}
2020-01-21 20:46:01 +00:00
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
// TODO: return a failed Response instead
return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported")
}
func (b *backend) Put(r *http.Request) (*internal.Href, error) {
2023-12-19 20:29:28 +00:00
wc, err := b.FileSystem.Create(r.Context(), r.URL.Path)
2020-01-21 20:46:01 +00:00
if err != nil {
return nil, err
2020-01-21 20:46:01 +00:00
}
defer wc.Close()
if _, err := io.Copy(wc, r.Body); err != nil {
return nil, err
2020-01-21 20:46:01 +00:00
}
return nil, wc.Close()
2020-01-21 20:46:01 +00:00
}
func (b *backend) Delete(r *http.Request) error {
2023-12-19 20:29:28 +00:00
err := b.FileSystem.RemoveAll(r.Context(), r.URL.Path)
2020-01-21 20:46:01 +00:00
if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
}
return err
2020-01-21 20:46:01 +00:00
}
2020-01-21 21:05:59 +00:00
func (b *backend) Mkcol(r *http.Request) error {
if r.Header.Get("Content-Type") != "" {
return internal.HTTPErrorf(http.StatusUnsupportedMediaType, "webdav: request body not supported in MKCOL request")
}
2023-12-19 20:29:28 +00:00
err := b.FileSystem.Mkdir(r.Context(), r.URL.Path)
2020-01-21 21:05:59 +00:00
if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusConflict, Err: err}
}
return err
}
2020-01-22 10:43:36 +00:00
2020-01-22 12:00:42 +00:00
func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) {
2023-12-19 20:29:28 +00:00
created, err = b.FileSystem.Copy(r.Context(), r.URL.Path, dest.Path, recursive, overwrite)
2020-01-22 12:00:42 +00:00
if os.IsExist(err) {
return false, &internal.HTTPError{http.StatusPreconditionFailed, err}
}
return created, err
}
2020-01-22 10:43:36 +00:00
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
2023-12-19 20:29:28 +00:00
created, err = b.FileSystem.MoveAll(r.Context(), r.URL.Path, dest.Path, overwrite)
2020-01-22 10:43:36 +00:00
if os.IsExist(err) {
return false, &internal.HTTPError{http.StatusPreconditionFailed, err}
}
return created, err
}
2022-03-21 08:16:50 +00:00
// BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a
// CardDAV addressbook-home-set. It should only be created via
2024-01-08 13:35:19 +00:00
// caldav.NewCalendarHomeSet or carddav.NewAddressbookHomeSet. Only to
2022-03-21 08:16:50 +00:00
// be used server-side, for listing a user's home sets as determined by the
// (external) backend.
type BackendSuppliedHomeSet interface {
GetXMLName() xml.Name
}
// UserPrincipalBackend can determine the current user's principal URL for a
// given request context.
type UserPrincipalBackend interface {
CurrentUserPrincipal(ctx context.Context) (string, error)
}
2024-01-08 13:35:19 +00:00
// Capability indicates the features that a server supports.
type Capability string
2024-01-08 13:35:19 +00:00
// ServePrincipalOptions holds options for ServePrincipal.
type ServePrincipalOptions struct {
CurrentUserPrincipalPath string
HomeSets []BackendSuppliedHomeSet
Capabilities []Capability
2022-03-21 08:16:50 +00:00
}
// ServePrincipal replies to requests for a principal URL.
func ServePrincipal(w http.ResponseWriter, r *http.Request, options *ServePrincipalOptions) {
2022-03-21 08:16:50 +00:00
switch r.Method {
case http.MethodOptions:
caps := []string{"1", "3"}
for _, c := range options.Capabilities {
caps = append(caps, string(c))
}
allow := []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}
2022-03-21 08:16:50 +00:00
w.Header().Add("DAV", strings.Join(caps, ", "))
w.Header().Add("Allow", strings.Join(allow, ", "))
w.WriteHeader(http.StatusNoContent)
case "PROPFIND":
if err := servePrincipalPropfind(w, r, options); err != nil {
2022-03-21 08:16:50 +00:00
internal.ServeError(w, err)
}
default:
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
}
}
func servePrincipalPropfind(w http.ResponseWriter, r *http.Request, options *ServePrincipalOptions) error {
var propfind internal.PropFind
2022-03-21 08:16:50 +00:00
if err := internal.DecodeXMLRequest(r, &propfind); err != nil {
return err
}
props := map[xml.Name]internal.PropFindFunc{
2022-03-21 08:16:50 +00:00
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(principalName), nil
},
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: options.CurrentUserPrincipalPath}}, nil
2022-03-21 08:16:50 +00:00
},
}
// TODO: handle Depth and more properties
for _, homeSet := range options.HomeSets {
hs := homeSet // capture variable for closure
props[homeSet.GetXMLName()] = func(*internal.RawXMLValue) (interface{}, error) {
return hs, nil
}
}
resp, err := internal.NewPropFindResponse(r.URL.Path, &propfind, props)
2022-03-21 08:16:50 +00:00
if err != nil {
return err
}
ms := internal.NewMultiStatus(*resp)
return internal.ServeMultiStatus(w, ms)
2022-03-21 08:16:50 +00:00
}