webdav: implement COPY and MOVE.

Also add a -port flag to litmus_test_server.

13 of 13 copymove tests from the litmus suite pass, as does 16 of 16
basic tests.

Change-Id: Idf92cad281e15db7d4d62e28e366ea7bfa89e564
Reviewed-on: https://go-review.googlesource.com/3470
Reviewed-by: Nick Cooper <nmvc@google.com>
Reviewed-by: Robert Stepanek <robert.stepanek@gmail.com>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/webdav/webdav.go b/webdav/webdav.go
index 93d971f..501b6aa 100644
--- a/webdav/webdav.go
+++ b/webdav/webdav.go
@@ -11,6 +11,7 @@
 	"errors"
 	"io"
 	"net/http"
+	"net/url"
 	"os"
 	"time"
 )
@@ -37,8 +38,7 @@
 	} else if h.LockSystem == nil {
 		status, err = http.StatusInternalServerError, errNoLockSystem
 	} else {
-		// TODO: COPY, MOVE, PROPFIND, PROPPATCH methods.
-		// MOVE needs to enforce its Depth constraint. See the parseDepth comment.
+		// TODO: PROPFIND, PROPPATCH methods.
 		switch r.Method {
 		case "OPTIONS":
 			status, err = h.handleOptions(w, r)
@@ -50,6 +50,8 @@
 			status, err = h.handlePut(w, r)
 		case "MKCOL":
 			status, err = h.handleMkcol(w, r)
+		case "COPY", "MOVE":
+			status, err = h.handleCopyMove(w, r)
 		case "LOCK":
 			status, err = h.handleLock(w, r)
 		case "UNLOCK":
@@ -193,6 +195,91 @@
 	return http.StatusCreated, nil
 }
 
+func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	// TODO: COPY/MOVE for Properties, as per sections 9.8.2 and 9.9.1.
+
+	hdr := r.Header.Get("Destination")
+	if hdr == "" {
+		return http.StatusBadRequest, errInvalidDestination
+	}
+	u, err := url.Parse(hdr)
+	if err != nil {
+		return http.StatusBadRequest, errInvalidDestination
+	}
+	if u.Host != r.Host {
+		return http.StatusBadGateway, errInvalidDestination
+	}
+	// TODO: do we need a webdav.StripPrefix HTTP handler that's like the
+	// standard library's http.StripPrefix handler, but also strips the
+	// prefix in the Destination header?
+
+	dst, src := u.Path, r.URL.Path
+	if dst == src {
+		return http.StatusForbidden, errDestinationEqualsSource
+	}
+
+	// TODO: confirmLocks should also check dst.
+	releaser, status, err := h.confirmLocks(r)
+	if err != nil {
+		return status, err
+	}
+	defer releaser.Release()
+
+	if r.Method == "COPY" {
+		// Section 9.8.3 says that "The COPY method on a collection without a Depth
+		// header must act as if a Depth header with value "infinity" was included".
+		depth := infiniteDepth
+		if hdr := r.Header.Get("Depth"); hdr != "" {
+			depth = parseDepth(hdr)
+			if depth != 0 && depth != infiniteDepth {
+				// Section 9.8.3 says that "A client may submit a Depth header on a
+				// COPY on a collection with a value of "0" or "infinity"."
+				return http.StatusBadRequest, errInvalidDepth
+			}
+		}
+		return copyFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
+	}
+
+	// Section 9.9.2 says that "The MOVE method on a collection must act as if
+	// a "Depth: infinity" header was used on it. A client must not submit a
+	// Depth header on a MOVE on a collection with any value but "infinity"."
+	if hdr := r.Header.Get("Depth"); hdr != "" {
+		if parseDepth(hdr) != infiniteDepth {
+			return http.StatusBadRequest, errInvalidDepth
+		}
+	}
+
+	created := false
+	if _, err := h.FileSystem.Stat(dst); err != nil {
+		if !os.IsNotExist(err) {
+			return http.StatusForbidden, err
+		}
+		created = true
+	} else {
+		switch r.Header.Get("Overwrite") {
+		case "T":
+			// Section 9.9.3 says that "If a resource exists at the destination
+			// and the Overwrite header is "T", then prior to performing the move,
+			// the server must perform a DELETE with "Depth: infinity" on the
+			// destination resource.
+			if err := h.FileSystem.RemoveAll(dst); err != nil {
+				return http.StatusForbidden, err
+			}
+		case "F":
+			return http.StatusPreconditionFailed, os.ErrExist
+		default:
+			return http.StatusBadRequest, errInvalidOverwrite
+		}
+	}
+	if err := h.FileSystem.Rename(src, dst); err != nil {
+		return http.StatusForbidden, err
+	}
+	if created {
+		return http.StatusCreated, nil
+	}
+	return http.StatusNoContent, 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 {
@@ -308,7 +395,8 @@
 //
 // Different WebDAV methods have further constraints on valid depths:
 //	- PROPFIND has no further restrictions, as per section 9.1.
-//	- MOVE accepts only "infinity", as per section 9.2.2.
+//	- COPY accepts only "0" or "infinity", as per section 9.8.3.
+//	- MOVE accepts only "infinity", as per section 9.9.2.
 //	- LOCK accepts only "0" or "infinity", as per section 9.10.3.
 // These constraints are enforced by the handleXxx methods.
 func parseDepth(s string) int {
@@ -349,16 +437,20 @@
 }
 
 var (
-	errDirectoryNotEmpty   = errors.New("webdav: directory not empty")
-	errInvalidDepth        = errors.New("webdav: invalid depth")
-	errInvalidIfHeader     = errors.New("webdav: invalid If header")
-	errInvalidLockInfo     = errors.New("webdav: invalid lock info")
-	errInvalidLockToken    = errors.New("webdav: invalid lock token")
-	errInvalidPropfind     = errors.New("webdav: invalid propfind")
-	errInvalidResponse     = errors.New("webdav: invalid response")
-	errInvalidTimeout      = errors.New("webdav: invalid timeout")
-	errNoFileSystem        = errors.New("webdav: no file system")
-	errNoLockSystem        = errors.New("webdav: no lock system")
-	errNotADirectory       = errors.New("webdav: not a directory")
-	errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
+	errDestinationEqualsSource = errors.New("webdav: destination equals source")
+	errDirectoryNotEmpty       = errors.New("webdav: directory not empty")
+	errInvalidDepth            = errors.New("webdav: invalid depth")
+	errInvalidDestination      = errors.New("webdav: invalid destination")
+	errInvalidIfHeader         = errors.New("webdav: invalid If header")
+	errInvalidLockInfo         = errors.New("webdav: invalid lock info")
+	errInvalidLockToken        = errors.New("webdav: invalid lock token")
+	errInvalidOverwrite        = errors.New("webdav: invalid overwrite")
+	errInvalidPropfind         = errors.New("webdav: invalid propfind")
+	errInvalidResponse         = errors.New("webdav: invalid response")
+	errInvalidTimeout          = errors.New("webdav: invalid timeout")
+	errNoFileSystem            = errors.New("webdav: no file system")
+	errNoLockSystem            = errors.New("webdav: no lock system")
+	errNotADirectory           = errors.New("webdav: not a directory")
+	errRecursionTooDeep        = errors.New("webdav: recursion too deep")
+	errUnsupportedLockInfo     = errors.New("webdav: unsupported lock info")
 )