From 326c4b9b6ff352da8cf8ff3375e5b508770b3c53 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 17 Jan 2020 11:30:42 +0100 Subject: [PATCH] internal: add Handler --- fs_local.go | 6 +- internal/server.go | 99 +++++++++++++++++++++++++++++++++ server.go | 133 ++++++++++++--------------------------------- 3 files changed, 138 insertions(+), 100 deletions(-) create mode 100644 internal/server.go diff --git a/fs_local.go b/fs_local.go index bf3a097..c7ab3ef 100644 --- a/fs_local.go +++ b/fs_local.go @@ -6,17 +6,19 @@ import ( "path" "path/filepath" "strings" + + "github.com/emersion/go-webdav/internal" ) 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") + return "", internal.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 "", internal.HTTPErrorf(http.StatusBadRequest, "webdav: expected absolute path") } return filepath.Join(string(fs), filepath.FromSlash(name)), nil } diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..bf5cf1d --- /dev/null +++ b/internal/server.go @@ -0,0 +1,99 @@ +package internal + +import ( + "net/http" + "fmt" + "mime" + "encoding/xml" +) + +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 { + s := fmt.Sprintf("%v %v", err.Code, http.StatusText(err.Code)) + if err.Err != nil { + return fmt.Sprintf("%v: %v", s, err.Err) + } else { + return s + } +} + +type Backend interface { + HeadGet(w http.ResponseWriter, r *http.Request) error + Propfind(r *http.Request, pf *Propfind, depth Depth) (*Multistatus, error) +} + +type Handler struct { + Backend Backend +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var err error + if h.Backend == nil { + err = fmt.Errorf("webdav: no backend available") + } else { + switch r.Method { + case http.MethodOptions: + err = h.handleOptions(w, r) + case http.MethodGet, http.MethodHead: + err = h.Backend.HeadGet(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, PROPFIND") + w.Header().Add("DAV", "1, 3") + w.WriteHeader(http.StatusNoContent) + 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 Propfind + if err := xml.NewDecoder(r.Body).Decode(&propfind); err != nil { + return &HTTPError{http.StatusBadRequest, err} + } + + depth := DepthInfinity + if s := r.Header.Get("Depth"); s != "" { + var err error + depth, err = ParseDepth(s) + if err != nil { + return &HTTPError{http.StatusBadRequest, err} + } + } + + ms, err := h.Backend.Propfind(r, &propfind, depth) + if err != nil { + return err + } + + 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) +} diff --git a/server.go b/server.go index 24a50cd..f247c5d 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,6 @@ package webdav import ( "encoding/xml" - "fmt" "mime" "net/http" "os" @@ -11,24 +10,6 @@ import ( "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 { - s := fmt.Sprintf("%v %v", err.Code, http.StatusText(err.Code)) - if err.Err != nil { - return fmt.Sprintf("%v: %v", s, err.Err) - } else { - return s - } -} - type File interface { http.File } @@ -42,40 +23,22 @@ type Handler struct { } 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") - } + http.Error(w, "webdav: no filesystem available", http.StatusInternalServerError) + return } - if err != nil { - code := http.StatusInternalServerError - if httpErr, ok := err.(*HTTPError); ok { - code = httpErr.Code - } - http.Error(w, err.Error(), code) - } + b := backend{h.FileSystem} + hh := internal.Handler{&b} + hh.ServeHTTP(w, r) } -func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) error { - w.Header().Add("Allow", "OPTIONS, GET, HEAD, PROPFIND") - w.Header().Add("DAV", "1, 3") - w.WriteHeader(http.StatusNoContent) - return nil +type backend struct { + FileSystem FileSystem } -func (h *Handler) handleGetHead(w http.ResponseWriter, r *http.Request) error { - f, err := h.FileSystem.Open(r.URL.Path) +func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error { + f, err := b.FileSystem.Open(r.URL.Path) if err != nil { return err } @@ -90,56 +53,30 @@ func (h *Handler) handleGetHead(w http.ResponseWriter, r *http.Request) error { 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} - } - } - - // TODO: refuse DepthInfinity, can cause infinite loops with symlinks - - f, err := h.FileSystem.Open(r.URL.Path) +func (b *backend) Propfind(r *http.Request, propfind *internal.Propfind, depth internal.Depth) (*internal.Multistatus, error) { + f, err := b.FileSystem.Open(r.URL.Path) if err != nil { - return err + return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { - return err + return nil, err } var resps []internal.Response - if err := h.propfind(&propfind, r.URL.Path, fi, depth, &resps); err != nil { - return err + if err := b.propfind(propfind, r.URL.Path, fi, depth, &resps); err != nil { + return nil, err } - ms := internal.NewMultistatus(resps...) - - 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) + return internal.NewMultistatus(resps...), nil } -func (h *Handler) propfind(propfind *internal.Propfind, name string, fi os.FileInfo, depth internal.Depth, resps *[]internal.Response) error { +func (b *backend) propfind(propfind *internal.Propfind, name string, fi os.FileInfo, depth internal.Depth, resps *[]internal.Response) error { // TODO: use partial error Response on error - resp, err := h.propfindFile(propfind, name, fi) + resp, err := b.propfindFile(propfind, name, fi) if err != nil { return err } @@ -151,7 +88,7 @@ func (h *Handler) propfind(propfind *internal.Propfind, name string, fi os.FileI childDepth = internal.DepthZero } - f, err := h.FileSystem.Open(name) + f, err := b.FileSystem.Open(name) if err != nil { return err } @@ -163,7 +100,7 @@ func (h *Handler) propfind(propfind *internal.Propfind, name string, fi os.FileI } for _, child := range children { - if err := h.propfind(propfind, path.Join(name, child.Name()), child, childDepth, resps); err != nil { + if err := b.propfind(propfind, path.Join(name, child.Name()), child, childDepth, resps); err != nil { return err } } @@ -172,14 +109,14 @@ func (h *Handler) propfind(propfind *internal.Propfind, name string, fi os.FileI return nil } -func (h *Handler) propfindFile(propfind *internal.Propfind, name string, fi os.FileInfo) (*internal.Response, error) { +func (b *backend) propfindFile(propfind *internal.Propfind, name string, fi os.FileInfo) (*internal.Response, error) { resp := internal.NewOKResponse(name) if propfind.PropName != nil { for xmlName, f := range liveProps { emptyVal := internal.NewRawXMLElement(xmlName, nil, nil) - _, err := f(h, name, fi) + _, err := f(b, name, fi) if err != nil { continue } @@ -191,7 +128,7 @@ func (h *Handler) propfindFile(propfind *internal.Propfind, name string, fi os.F } else if propfind.AllProp != nil { // TODO: add support for propfind.Include for _, f := range liveProps { - val, err := f(h, name, fi) + val, err := f(b, name, fi) if err != nil { continue } @@ -208,9 +145,9 @@ func (h *Handler) propfindFile(propfind *internal.Propfind, name string, fi os.F var val interface{} = emptyVal f, ok := liveProps[xmlName] if ok { - if v, err := f(h, name, fi); err != nil { + if v, err := f(b, name, fi); err != nil { // TODO: don't throw away error message here - if httpErr, ok := err.(*HTTPError); ok { + if httpErr, ok := err.(*internal.HTTPError); ok { code = httpErr.Code } else { code = http.StatusInternalServerError @@ -228,42 +165,42 @@ func (h *Handler) propfindFile(propfind *internal.Propfind, name string, fi os.F } } } else { - return nil, HTTPErrorf(http.StatusBadRequest, "webdav: propfind request missing propname, allprop or prop element") + return nil, internal.HTTPErrorf(http.StatusBadRequest, "webdav: propfind request missing propname, allprop or prop element") } return resp, nil } -type PropfindFunc func(h *Handler, name string, fi os.FileInfo) (interface{}, error) +type PropfindFunc func(b *backend, 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) { + {"DAV:", "resourcetype"}: func(b *backend, name string, fi os.FileInfo) (interface{}, error) { var types []xml.Name if fi.IsDir() { types = append(types, internal.CollectionName) } return internal.NewResourceType(types...), nil }, - {"DAV:", "getcontentlength"}: func(h *Handler, name string, fi os.FileInfo) (interface{}, error) { + {"DAV:", "getcontentlength"}: func(b *backend, name string, fi os.FileInfo) (interface{}, error) { if fi.IsDir() { - return nil, &HTTPError{Code: http.StatusNotFound} + return nil, &internal.HTTPError{Code: http.StatusNotFound} } return &internal.GetContentLength{Length: fi.Size()}, nil }, - {"DAV:", "getcontenttype"}: func(h *Handler, name string, fi os.FileInfo) (interface{}, error) { + {"DAV:", "getcontenttype"}: func(b *backend, name string, fi os.FileInfo) (interface{}, error) { if fi.IsDir() { - return nil, &HTTPError{Code: http.StatusNotFound} + return nil, &internal.HTTPError{Code: http.StatusNotFound} } t := mime.TypeByExtension(path.Ext(name)) if t == "" { // TODO: use http.DetectContentType - return nil, &HTTPError{Code: http.StatusNotFound} + return nil, &internal.HTTPError{Code: http.StatusNotFound} } return &internal.GetContentType{Type: t}, nil }, - {"DAV:", "getlastmodified"}: func(h *Handler, name string, fi os.FileInfo) (interface{}, error) { + {"DAV:", "getlastmodified"}: func(b *backend, name string, fi os.FileInfo) (interface{}, error) { if fi.IsDir() { - return nil, &HTTPError{Code: http.StatusNotFound} + return nil, &internal.HTTPError{Code: http.StatusNotFound} } return &internal.GetLastModified{LastModified: internal.Time(fi.ModTime())}, nil },