// 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. 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") )