Index: webdav/webdav.go |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/webdav/webdav.go |
@@ -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") |
+) |