webdav: add minimal server implementation

This commit is contained in:
Simon Ser 2020-01-15 18:21:27 +01:00
parent 42765234a8
commit ae93da82c1
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
4 changed files with 296 additions and 8 deletions

32
fs_local.go Normal file
View File

@ -0,0 +1,32 @@
package webdav
import (
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
type LocalFileSystem string
func (fs LocalFileSystem) path(name string) (string, error) {
if (filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0) || strings.Contains(name, "\x00") {
return "", HTTPErrorf(http.StatusBadRequest, "webdav: invalid character in path")
}
name = path.Clean(name)
if !path.IsAbs(name) {
return "", HTTPErrorf(http.StatusBadRequest, "webdav: expected absolute path")
}
return filepath.Join(string(fs), filepath.FromSlash(name)), nil
}
func (fs LocalFileSystem) Open(name string) (File, error) {
p, err := fs.path(name)
if err != nil {
return nil, err
}
return os.Open(p)
}
var _ FileSystem = LocalFileSystem("")

View File

@ -25,6 +25,19 @@ const (
DepthInfinity Depth = -1 DepthInfinity Depth = -1
) )
// ParseDepth parses a Depth header.
func ParseDepth(s string) (Depth, error) {
switch s {
case "0":
return DepthZero, nil
case "1":
return DepthOne, nil
case "infinity":
return DepthInfinity, nil
}
return 0, fmt.Errorf("webdav: invalid Depth value")
}
// String formats the depth. // String formats the depth.
func (d Depth) String() string { func (d Depth) String() string {
switch d { switch d {

View File

@ -8,23 +8,40 @@ import (
"strings" "strings"
) )
// TODO: cache parsed value
type Status string type Status string
func (s Status) Err() error { func NewStatus(code int, msg string) Status {
if msg == "" {
msg = http.StatusText(code)
}
return Status(fmt.Sprintf("HTTP/1.1 %v %v", code, msg))
}
func (s Status) parse() (int, string, error) {
if s == "" { if s == "" {
return nil return http.StatusOK, "", nil
} }
parts := strings.SplitN(string(s), " ", 3) parts := strings.SplitN(string(s), " ", 3)
if len(parts) != 3 { if len(parts) != 3 {
return fmt.Errorf("webdav: invalid HTTP status %q: expected 3 fields", s) return 0, "", fmt.Errorf("webdav: invalid HTTP status %q: expected 3 fields", s)
} }
code, err := strconv.Atoi(parts[1]) code, err := strconv.Atoi(parts[1])
if err != nil { if err != nil {
return fmt.Errorf("webdav: invalid HTTP status %q: failed to parse code: %v", s, err) return 0, "", fmt.Errorf("webdav: invalid HTTP status %q: failed to parse code: %v", s, err)
} }
msg := parts[2] msg := parts[2]
return code, msg, nil
}
func (s Status) Err() error {
code, msg, err := s.parse()
if err != nil {
return err
}
// TODO: handle 2xx, 3xx // TODO: handle 2xx, 3xx
if code != http.StatusOK { if code != http.StatusOK {
return fmt.Errorf("webdav: HTTP error: %v %v", code, msg) return fmt.Errorf("webdav: HTTP error: %v %v", code, msg)
@ -39,6 +56,10 @@ type Multistatus struct {
ResponseDescription string `xml:"responsedescription,omitempty"` ResponseDescription string `xml:"responsedescription,omitempty"`
} }
func NewMultistatus(resps ...Response) *Multistatus {
return &Multistatus{Responses: resps}
}
func (ms *Multistatus) Get(href string) (*Response, error) { func (ms *Multistatus) Get(href string) (*Response, error) {
for i := range ms.Responses { for i := range ms.Responses {
resp := &ms.Responses[i] resp := &ms.Responses[i]
@ -63,6 +84,13 @@ type Response struct {
Location *Location `xml:"location,omitempty"` Location *Location `xml:"location,omitempty"`
} }
func NewOKResponse(href string) *Response {
return &Response{
Hrefs: []string{href},
Status: NewStatus(http.StatusOK, ""),
}
}
func (resp *Response) Href() (string, error) { func (resp *Response) Href() (string, error) {
if err := resp.Status.Err(); err != nil { if err := resp.Status.Err(); err != nil {
return "", err return "", err
@ -97,6 +125,28 @@ func (resp *Response) DecodeProp(v interface{}) error {
return fmt.Errorf("webdav: missing prop %v %v in response", name.Space, name.Local) return fmt.Errorf("webdav: missing prop %v %v in response", name.Space, name.Local)
} }
func (resp *Response) EncodeProp(code int, v interface{}) error {
raw, err := EncodeRawXMLElement(v)
if err != nil {
return err
}
for i := range resp.Propstats {
propstat := &resp.Propstats[i]
c, _, _ := propstat.Status.parse()
if c == code {
propstat.Prop.Raw = append(propstat.Prop.Raw, *raw)
return nil
}
}
resp.Propstats = append(resp.Propstats, Propstat{
Status: NewStatus(code, ""),
Prop: Prop{Raw: []RawXMLValue{*raw}},
})
return nil
}
// https://tools.ietf.org/html/rfc4918#section-14.9 // https://tools.ietf.org/html/rfc4918#section-14.9
type Location struct { type Location struct {
XMLName xml.Name `xml:"DAV: location"` XMLName xml.Name `xml:"DAV: location"`
@ -130,6 +180,16 @@ func EncodeProp(values ...interface{}) (*Prop, error) {
return &Prop{Raw: l}, nil return &Prop{Raw: l}, nil
} }
func (prop *Prop) XMLNames() []xml.Name {
l := make([]xml.Name, 0, len(prop.Raw))
for _, raw := range prop.Raw {
if start, ok := raw.tok.(xml.StartElement); ok {
l = append(l, start.Name)
}
}
return l
}
// https://tools.ietf.org/html/rfc4918#section-14.20 // https://tools.ietf.org/html/rfc4918#section-14.20
type Propfind struct { type Propfind struct {
XMLName xml.Name `xml:"DAV: propfind"` XMLName xml.Name `xml:"DAV: propfind"`
@ -137,12 +197,16 @@ type Propfind struct {
// TODO: propname | (allprop, include?) // TODO: propname | (allprop, include?)
} }
func NewPropNamePropfind(names ...xml.Name) *Propfind { func xmlNamesToRaw(names []xml.Name) []RawXMLValue {
children := make([]RawXMLValue, len(names)) l := make([]RawXMLValue, len(names))
for i, name := range names { for i, name := range names {
children[i] = *NewRawXMLElement(name, nil, nil) l[i] = *NewRawXMLElement(name, nil, nil)
} }
return &Propfind{Prop: &Prop{Raw: children}} return l
}
func NewPropNamePropfind(names ...xml.Name) *Propfind {
return &Propfind{Prop: &Prop{Raw: xmlNamesToRaw(names)}}
} }
// https://tools.ietf.org/html/rfc4918#section-15.9 // https://tools.ietf.org/html/rfc4918#section-15.9
@ -151,6 +215,10 @@ type ResourceType struct {
Raw []RawXMLValue `xml:",any"` Raw []RawXMLValue `xml:",any"`
} }
func NewResourceType(names ...xml.Name) *ResourceType {
return &ResourceType{Raw: xmlNamesToRaw(names)}
}
func (t *ResourceType) Is(name xml.Name) bool { func (t *ResourceType) Is(name xml.Name) bool {
for _, raw := range t.Raw { for _, raw := range t.Raw {
if start, ok := raw.tok.(xml.StartElement); ok && name == start.Name { if start, ok := raw.tok.(xml.StartElement); ok && name == start.Name {

175
server.go Normal file
View File

@ -0,0 +1,175 @@
package webdav
import (
"encoding/xml"
"fmt"
"mime"
"net/http"
"os"
"github.com/emersion/go-webdav/internal"
)
type HTTPError struct {
Code int
Err error
}
func HTTPErrorf(code int, format string, a ...interface{}) *HTTPError {
return &HTTPError{code, fmt.Errorf(format, a...)}
}
func (err *HTTPError) Error() string {
return fmt.Sprintf("%v %v: %v", err.Code, http.StatusText(err.Code), err.Err)
}
type File interface {
http.File
}
type FileSystem interface {
Open(name string) (File, error)
}
type Handler struct {
FileSystem FileSystem
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
if h.FileSystem == nil {
err = HTTPErrorf(http.StatusInternalServerError, "webdav: no filesystem available")
} else {
switch r.Method {
case http.MethodOptions:
err = h.handleOptions(w, r)
case http.MethodGet, http.MethodHead:
err = h.handleGetHead(w, r)
case "PROPFIND":
err = h.handlePropfind(w, r)
default:
err = HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
}
}
if err != nil {
code := http.StatusInternalServerError
if httpErr, ok := err.(*HTTPError); ok {
code = httpErr.Code
}
http.Error(w, err.Error(), code)
}
}
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) error {
w.Header().Add("Allow", "OPTIONS, GET, HEAD")
w.Header().Add("DAV", "1")
w.WriteHeader(http.StatusNoContent)
return nil
}
func (h *Handler) handleGetHead(w http.ResponseWriter, r *http.Request) error {
f, err := h.FileSystem.Open(r.URL.Path)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f)
return nil
}
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if t != "application/xml" && t != "text/xml" {
return HTTPErrorf(http.StatusBadRequest, "webdav: expected application/xml PROPFIND request")
}
var propfind internal.Propfind
if err := xml.NewDecoder(r.Body).Decode(&propfind); err != nil {
return &HTTPError{http.StatusBadRequest, err}
}
depth := internal.DepthInfinity
if s := r.Header.Get("Depth"); s != "" {
var err error
depth, err = internal.ParseDepth(s)
if err != nil {
return &HTTPError{http.StatusBadRequest, err}
}
}
f, err := h.FileSystem.Open(r.URL.Path)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
if depth != internal.DepthZero {
depth = internal.DepthZero // TODO
}
resp, err := h.propfindFile(&propfind, r.URL.Path, fi)
if err != nil {
return err
}
ms := internal.NewMultistatus(*resp)
w.Header().Add("Content-Type", "text/xml; charset=\"utf-8\"")
w.WriteHeader(http.StatusMultiStatus)
w.Write([]byte(xml.Header))
return xml.NewEncoder(w).Encode(&ms)
}
func (h *Handler) propfindFile(propfind *internal.Propfind, name string, fi os.FileInfo) (*internal.Response, error) {
resp := internal.NewOKResponse(name)
if prop := propfind.Prop; prop != nil {
for _, xmlName := range prop.XMLNames() {
emptyVal := internal.NewRawXMLElement(xmlName, nil, nil)
var code int
var val interface{} = emptyVal
f, ok := liveProps[xmlName]
if ok {
if v, err := f(h, name, fi); err != nil {
code = http.StatusInternalServerError // TODO: better error handling
} else {
code = http.StatusOK
val = v
}
} else {
code = http.StatusNotFound
}
if err := resp.EncodeProp(code, val); err != nil {
return nil, err
}
}
}
return resp, nil
}
type PropfindFunc func(h *Handler, name string, fi os.FileInfo) (interface{}, error)
var liveProps = map[xml.Name]PropfindFunc{
{"DAV:", "resourcetype"}: func(h *Handler, name string, fi os.FileInfo) (interface{}, error) {
var types []xml.Name
if fi.IsDir() {
types = append(types, internal.CollectionName)
}
return internal.NewResourceType(types...), nil
},
}