blob: 4ce09728bc507681ee962845517b2be5e5cff792 [file] [log] [blame]
Nigel Taobaf9fd42014-11-11 17:46:57 +11001// Copyright 2014 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Nigel Tao9622a222016-04-07 11:54:59 +10005// Package webdav provides a WebDAV server implementation.
David Symonds8aa6e202014-12-09 14:17:11 +11006package webdav // import "golang.org/x/net/webdav"
Nigel Taobaf9fd42014-11-11 17:46:57 +11007
Nigel Taobaf9fd42014-11-11 17:46:57 +11008import (
9 "errors"
Robert Stepanek7dbad502015-01-28 21:50:26 +010010 "fmt"
Nigel Taobaf9fd42014-11-11 17:46:57 +110011 "io"
12 "net/http"
Nigel Taocd216762015-01-29 14:43:00 +110013 "net/url"
Nigel Taobaf9fd42014-11-11 17:46:57 +110014 "os"
Federico Simoncellidb8e4de2015-08-24 18:07:02 +020015 "path"
Nigel Tao5273a782015-05-19 18:10:47 +100016 "strings"
Nigel Taobaf9fd42014-11-11 17:46:57 +110017 "time"
18)
19
Nigel Taobaf9fd42014-11-11 17:46:57 +110020type Handler struct {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +020021 // Prefix is the URL path prefix to strip from WebDAV resource paths.
22 Prefix string
Nigel Taobaf9fd42014-11-11 17:46:57 +110023 // FileSystem is the virtual file system.
24 FileSystem FileSystem
25 // LockSystem is the lock management system.
26 LockSystem LockSystem
Nigel Taobaf9fd42014-11-11 17:46:57 +110027 // Logger is an optional error logger. If non-nil, it will be called
Nick Cooper7b488c12015-01-05 15:02:10 +110028 // for all HTTP requests.
Nigel Taobaf9fd42014-11-11 17:46:57 +110029 Logger func(*http.Request, error)
30}
31
Federico Simoncellidb8e4de2015-08-24 18:07:02 +020032func (h *Handler) stripPrefix(p string) (string, int, error) {
33 if h.Prefix == "" {
34 return p, http.StatusOK, nil
35 }
36 if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) {
37 return r, http.StatusOK, nil
38 }
39 return p, http.StatusNotFound, errPrefixMismatch
40}
41
Nigel Taobaf9fd42014-11-11 17:46:57 +110042func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Nigel Tao614fbbe2015-02-04 18:11:38 +110043 status, err := http.StatusBadRequest, errUnsupportedMethod
Nigel Taobaf9fd42014-11-11 17:46:57 +110044 if h.FileSystem == nil {
45 status, err = http.StatusInternalServerError, errNoFileSystem
46 } else if h.LockSystem == nil {
47 status, err = http.StatusInternalServerError, errNoLockSystem
48 } else {
Nigel Taobaf9fd42014-11-11 17:46:57 +110049 switch r.Method {
Nick Cooper7b488c12015-01-05 15:02:10 +110050 case "OPTIONS":
51 status, err = h.handleOptions(w, r)
Nigel Taobaf9fd42014-11-11 17:46:57 +110052 case "GET", "HEAD", "POST":
53 status, err = h.handleGetHeadPost(w, r)
54 case "DELETE":
55 status, err = h.handleDelete(w, r)
56 case "PUT":
57 status, err = h.handlePut(w, r)
58 case "MKCOL":
59 status, err = h.handleMkcol(w, r)
Nigel Taocd216762015-01-29 14:43:00 +110060 case "COPY", "MOVE":
61 status, err = h.handleCopyMove(w, r)
Nigel Taobaf9fd42014-11-11 17:46:57 +110062 case "LOCK":
63 status, err = h.handleLock(w, r)
64 case "UNLOCK":
65 status, err = h.handleUnlock(w, r)
Robert Stepanek7dbad502015-01-28 21:50:26 +010066 case "PROPFIND":
67 status, err = h.handlePropfind(w, r)
Robert Stepanek169f4222015-04-14 22:14:59 +020068 case "PROPPATCH":
69 status, err = h.handleProppatch(w, r)
Nigel Taobaf9fd42014-11-11 17:46:57 +110070 }
71 }
72
73 if status != 0 {
74 w.WriteHeader(status)
75 if status != http.StatusNoContent {
76 w.Write([]byte(StatusText(status)))
77 }
78 }
Nick Cooper7b488c12015-01-05 15:02:10 +110079 if h.Logger != nil {
Nigel Taobaf9fd42014-11-11 17:46:57 +110080 h.Logger(r, err)
81 }
82}
83
Nigel Tao00757942015-02-04 18:07:40 +110084func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) {
85 token, err = h.LockSystem.Create(now, LockDetails{
86 Root: root,
87 Duration: infiniteTimeout,
88 ZeroDepth: true,
89 })
90 if err != nil {
91 if err == ErrLocked {
92 return "", StatusLocked, err
93 }
94 return "", http.StatusInternalServerError, err
95 }
96 return token, 0, nil
97}
Nick Cooper7b488c12015-01-05 15:02:10 +110098
Nigel Tao00757942015-02-04 18:07:40 +110099func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {
Nick Cooper7b488c12015-01-05 15:02:10 +1100100 hdr := r.Header.Get("If")
101 if hdr == "" {
Nigel Tao00757942015-02-04 18:07:40 +1100102 // An empty If header means that the client hasn't previously created locks.
103 // Even if this client doesn't care about locks, we still need to check that
104 // the resources aren't locked by another client, so we create temporary
105 // locks that would conflict with another client's locks. These temporary
106 // locks are unlocked at the end of the HTTP request.
107 now, srcToken, dstToken := time.Now(), "", ""
108 if src != "" {
109 srcToken, status, err = h.lock(now, src)
110 if err != nil {
111 return nil, status, err
112 }
113 }
114 if dst != "" {
115 dstToken, status, err = h.lock(now, dst)
116 if err != nil {
117 if srcToken != "" {
118 h.LockSystem.Unlock(now, srcToken)
119 }
120 return nil, status, err
121 }
122 }
123
124 return func() {
125 if dstToken != "" {
126 h.LockSystem.Unlock(now, dstToken)
127 }
128 if srcToken != "" {
129 h.LockSystem.Unlock(now, srcToken)
130 }
131 }, 0, nil
Nick Cooper7b488c12015-01-05 15:02:10 +1100132 }
Nigel Tao00757942015-02-04 18:07:40 +1100133
Nick Cooper7b488c12015-01-05 15:02:10 +1100134 ih, ok := parseIfHeader(hdr)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100135 if !ok {
136 return nil, http.StatusBadRequest, errInvalidIfHeader
137 }
138 // ih is a disjunction (OR) of ifLists, so any ifList will do.
139 for _, l := range ih.lists {
Nigel Tao00757942015-02-04 18:07:40 +1100140 lsrc := l.resourceTag
141 if lsrc == "" {
142 lsrc = src
143 } else {
144 u, err := url.Parse(lsrc)
145 if err != nil {
146 continue
147 }
148 if u.Host != r.Host {
149 continue
150 }
Nigel Taod7bf3542016-06-13 14:23:54 +1000151 lsrc, status, err = h.stripPrefix(u.Path)
152 if err != nil {
153 return nil, status, err
154 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100155 }
Nigel Tao00757942015-02-04 18:07:40 +1100156 release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100157 if err == ErrConfirmationFailed {
158 continue
159 }
160 if err != nil {
161 return nil, http.StatusInternalServerError, err
162 }
Nigel Tao00757942015-02-04 18:07:40 +1100163 return release, 0, nil
Nigel Taobaf9fd42014-11-11 17:46:57 +1100164 }
Nigel Tao00757942015-02-04 18:07:40 +1100165 // Section 10.4.1 says that "If this header is evaluated and all state lists
166 // fail, then the request must fail with a 412 (Precondition Failed) status."
167 // We follow the spec even though the cond_put_corrupt_token test case from
168 // the litmus test warns on seeing a 412 instead of a 423 (Locked).
Nigel Tao46077d32015-01-07 16:53:29 +1100169 return nil, http.StatusPreconditionFailed, ErrLocked
Nigel Taobaf9fd42014-11-11 17:46:57 +1100170}
171
Nick Cooper7b488c12015-01-05 15:02:10 +1100172func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200173 reqPath, status, err := h.stripPrefix(r.URL.Path)
174 if err != nil {
175 return status, err
176 }
Nick Cooper7b488c12015-01-05 15:02:10 +1100177 allow := "OPTIONS, LOCK, PUT, MKCOL"
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200178 if fi, err := h.FileSystem.Stat(reqPath); err == nil {
Nick Cooper7b488c12015-01-05 15:02:10 +1100179 if fi.IsDir() {
Yasuhiro Matsumotod75b1902015-11-12 13:44:27 +0900180 allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND"
Nick Cooper7b488c12015-01-05 15:02:10 +1100181 } else {
Robert Stepanek1dfe7912015-01-29 15:12:46 +0100182 allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"
Nick Cooper7b488c12015-01-05 15:02:10 +1100183 }
184 }
Robert Stepanek1dfe7912015-01-29 15:12:46 +0100185 w.Header().Set("Allow", allow)
Nick Cooper7b488c12015-01-05 15:02:10 +1100186 // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes
187 w.Header().Set("DAV", "1, 2")
188 // http://msdn.microsoft.com/en-au/library/cc250217.aspx
189 w.Header().Set("MS-Author-Via", "DAV")
Nick Cooper7b488c12015-01-05 15:02:10 +1100190 return 0, nil
191}
192
Nigel Taobaf9fd42014-11-11 17:46:57 +1100193func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200194 reqPath, status, err := h.stripPrefix(r.URL.Path)
195 if err != nil {
196 return status, err
197 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100198 // TODO: check locks for read-only access??
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200199 f, err := h.FileSystem.OpenFile(reqPath, os.O_RDONLY, 0)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100200 if err != nil {
201 return http.StatusNotFound, err
202 }
203 defer f.Close()
204 fi, err := f.Stat()
205 if err != nil {
206 return http.StatusNotFound, err
207 }
Yasuhiro Matsumotod75b1902015-11-12 13:44:27 +0900208 if fi.IsDir() {
209 return http.StatusMethodNotAllowed, nil
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100210 }
Yasuhiro Matsumotod75b1902015-11-12 13:44:27 +0900211 etag, err := findETag(h.FileSystem, h.LockSystem, reqPath, fi)
212 if err != nil {
213 return http.StatusInternalServerError, err
214 }
215 w.Header().Set("ETag", etag)
Robert Stepanek610bfee2015-05-26 12:20:52 +0200216 // Let ServeContent determine the Content-Type header.
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200217 http.ServeContent(w, r, reqPath, fi.ModTime(), f)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100218 return 0, nil
219}
220
221func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200222 reqPath, status, err := h.stripPrefix(r.URL.Path)
223 if err != nil {
224 return status, err
225 }
226 release, status, err := h.confirmLocks(r, reqPath, "")
Nigel Taobaf9fd42014-11-11 17:46:57 +1100227 if err != nil {
228 return status, err
229 }
Nigel Tao00757942015-02-04 18:07:40 +1100230 defer release()
Nigel Taobaf9fd42014-11-11 17:46:57 +1100231
Nigel Tao1db34d82015-01-16 10:33:09 +1100232 // TODO: return MultiStatus where appropriate.
233
234 // "godoc os RemoveAll" says that "If the path does not exist, RemoveAll
235 // returns nil (no error)." WebDAV semantics are that it should return a
236 // "404 Not Found". We therefore have to Stat before we RemoveAll.
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200237 if _, err := h.FileSystem.Stat(reqPath); err != nil {
Nick Cooper7b488c12015-01-05 15:02:10 +1100238 if os.IsNotExist(err) {
239 return http.StatusNotFound, err
240 }
Nigel Tao1db34d82015-01-16 10:33:09 +1100241 return http.StatusMethodNotAllowed, err
242 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200243 if err := h.FileSystem.RemoveAll(reqPath); err != nil {
Nigel Taobaf9fd42014-11-11 17:46:57 +1100244 return http.StatusMethodNotAllowed, err
245 }
246 return http.StatusNoContent, nil
247}
248
249func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200250 reqPath, status, err := h.stripPrefix(r.URL.Path)
251 if err != nil {
252 return status, err
253 }
254 release, status, err := h.confirmLocks(r, reqPath, "")
Nigel Taobaf9fd42014-11-11 17:46:57 +1100255 if err != nil {
256 return status, err
257 }
Nigel Tao00757942015-02-04 18:07:40 +1100258 defer release()
Robert Stepanek610bfee2015-05-26 12:20:52 +0200259 // TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
260 // comments in http.checkEtag.
Nigel Taobaf9fd42014-11-11 17:46:57 +1100261
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200262 f, err := h.FileSystem.OpenFile(reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100263 if err != nil {
264 return http.StatusNotFound, err
265 }
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100266 _, copyErr := io.Copy(f, r.Body)
Robert Stepanek610bfee2015-05-26 12:20:52 +0200267 fi, statErr := f.Stat()
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100268 closeErr := f.Close()
Robert Stepanek610bfee2015-05-26 12:20:52 +0200269 // TODO(rost): Returning 405 Method Not Allowed might not be appropriate.
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100270 if copyErr != nil {
271 return http.StatusMethodNotAllowed, copyErr
Nigel Taobaf9fd42014-11-11 17:46:57 +1100272 }
Robert Stepanek610bfee2015-05-26 12:20:52 +0200273 if statErr != nil {
274 return http.StatusMethodNotAllowed, statErr
275 }
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100276 if closeErr != nil {
277 return http.StatusMethodNotAllowed, closeErr
278 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200279 etag, err := findETag(h.FileSystem, h.LockSystem, reqPath, fi)
Robert Stepanek84ba27d2015-02-15 15:15:54 +0100280 if err != nil {
281 return http.StatusInternalServerError, err
282 }
Robert Stepanek610bfee2015-05-26 12:20:52 +0200283 w.Header().Set("ETag", etag)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100284 return http.StatusCreated, nil
285}
286
287func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200288 reqPath, status, err := h.stripPrefix(r.URL.Path)
289 if err != nil {
290 return status, err
291 }
292 release, status, err := h.confirmLocks(r, reqPath, "")
Nigel Taobaf9fd42014-11-11 17:46:57 +1100293 if err != nil {
294 return status, err
295 }
Nigel Tao00757942015-02-04 18:07:40 +1100296 defer release()
Nigel Taobaf9fd42014-11-11 17:46:57 +1100297
Nick Cooper7b488c12015-01-05 15:02:10 +1100298 if r.ContentLength > 0 {
299 return http.StatusUnsupportedMediaType, nil
300 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200301 if err := h.FileSystem.Mkdir(reqPath, 0777); err != nil {
Nigel Taobaf9fd42014-11-11 17:46:57 +1100302 if os.IsNotExist(err) {
303 return http.StatusConflict, err
304 }
305 return http.StatusMethodNotAllowed, err
306 }
307 return http.StatusCreated, nil
308}
309
Nigel Taocd216762015-01-29 14:43:00 +1100310func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
Nigel Taocd216762015-01-29 14:43:00 +1100311 hdr := r.Header.Get("Destination")
312 if hdr == "" {
313 return http.StatusBadRequest, errInvalidDestination
314 }
315 u, err := url.Parse(hdr)
316 if err != nil {
317 return http.StatusBadRequest, errInvalidDestination
318 }
319 if u.Host != r.Host {
320 return http.StatusBadGateway, errInvalidDestination
321 }
Nigel Taocd216762015-01-29 14:43:00 +1100322
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200323 src, status, err := h.stripPrefix(r.URL.Path)
324 if err != nil {
325 return status, err
326 }
327
328 dst, status, err := h.stripPrefix(u.Path)
329 if err != nil {
330 return status, err
331 }
332
Nigel Tao00757942015-02-04 18:07:40 +1100333 if dst == "" {
334 return http.StatusBadGateway, errInvalidDestination
335 }
Nigel Taocd216762015-01-29 14:43:00 +1100336 if dst == src {
337 return http.StatusForbidden, errDestinationEqualsSource
338 }
339
Nigel Taocd216762015-01-29 14:43:00 +1100340 if r.Method == "COPY" {
Nigel Tao00757942015-02-04 18:07:40 +1100341 // Section 7.5.1 says that a COPY only needs to lock the destination,
342 // not both destination and source. Strictly speaking, this is racy,
343 // even though a COPY doesn't modify the source, if a concurrent
344 // operation modifies the source. However, the litmus test explicitly
345 // checks that COPYing a locked-by-another source is OK.
346 release, status, err := h.confirmLocks(r, "", dst)
347 if err != nil {
348 return status, err
349 }
350 defer release()
351
Nigel Taocd216762015-01-29 14:43:00 +1100352 // Section 9.8.3 says that "The COPY method on a collection without a Depth
353 // header must act as if a Depth header with value "infinity" was included".
354 depth := infiniteDepth
355 if hdr := r.Header.Get("Depth"); hdr != "" {
356 depth = parseDepth(hdr)
357 if depth != 0 && depth != infiniteDepth {
358 // Section 9.8.3 says that "A client may submit a Depth header on a
359 // COPY on a collection with a value of "0" or "infinity"."
360 return http.StatusBadRequest, errInvalidDepth
361 }
362 }
363 return copyFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
364 }
365
Nigel Tao00757942015-02-04 18:07:40 +1100366 release, status, err := h.confirmLocks(r, src, dst)
367 if err != nil {
368 return status, err
369 }
370 defer release()
371
Nigel Taocd216762015-01-29 14:43:00 +1100372 // Section 9.9.2 says that "The MOVE method on a collection must act as if
373 // a "Depth: infinity" header was used on it. A client must not submit a
374 // Depth header on a MOVE on a collection with any value but "infinity"."
375 if hdr := r.Header.Get("Depth"); hdr != "" {
376 if parseDepth(hdr) != infiniteDepth {
377 return http.StatusBadRequest, errInvalidDepth
378 }
379 }
Nigel Tao5b4754d2015-02-10 16:53:23 +1100380 return moveFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T")
Nigel Taocd216762015-01-29 14:43:00 +1100381}
382
Nigel Taobaf9fd42014-11-11 17:46:57 +1100383func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
384 duration, err := parseTimeout(r.Header.Get("Timeout"))
385 if err != nil {
386 return http.StatusBadRequest, err
387 }
388 li, status, err := readLockInfo(r.Body)
389 if err != nil {
390 return status, err
391 }
392
Nigel Tao5feaa292015-02-03 17:29:08 +1100393 token, ld, now, created := "", LockDetails{}, time.Now(), false
Nigel Taobaf9fd42014-11-11 17:46:57 +1100394 if li == (lockInfo{}) {
395 // An empty lockInfo means to refresh the lock.
396 ih, ok := parseIfHeader(r.Header.Get("If"))
397 if !ok {
398 return http.StatusBadRequest, errInvalidIfHeader
399 }
400 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
401 token = ih.lists[0].conditions[0].Token
402 }
403 if token == "" {
404 return http.StatusBadRequest, errInvalidLockToken
405 }
Nigel Tao46077d32015-01-07 16:53:29 +1100406 ld, err = h.LockSystem.Refresh(now, token, duration)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100407 if err != nil {
408 if err == ErrNoSuchLock {
409 return http.StatusPreconditionFailed, err
410 }
411 return http.StatusInternalServerError, err
412 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100413
414 } else {
Nigel Tao67f25492015-01-09 10:57:20 +1100415 // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
416 // then the request MUST act as if a "Depth:infinity" had been submitted."
417 depth := infiniteDepth
418 if hdr := r.Header.Get("Depth"); hdr != "" {
419 depth = parseDepth(hdr)
420 if depth != 0 && depth != infiniteDepth {
421 // Section 9.10.3 says that "Values other than 0 or infinity must not be
422 // used with the Depth header on a LOCK method".
423 return http.StatusBadRequest, errInvalidDepth
424 }
Nigel Tao46077d32015-01-07 16:53:29 +1100425 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200426 reqPath, status, err := h.stripPrefix(r.URL.Path)
427 if err != nil {
428 return status, err
429 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100430 ld = LockDetails{
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200431 Root: reqPath,
Nigel Tao67f25492015-01-09 10:57:20 +1100432 Duration: duration,
433 OwnerXML: li.Owner.InnerXML,
434 ZeroDepth: depth == 0,
Nigel Taobaf9fd42014-11-11 17:46:57 +1100435 }
Nigel Tao46077d32015-01-07 16:53:29 +1100436 token, err = h.LockSystem.Create(now, ld)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100437 if err != nil {
Nigel Tao46077d32015-01-07 16:53:29 +1100438 if err == ErrLocked {
439 return StatusLocked, err
440 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100441 return http.StatusInternalServerError, err
442 }
443 defer func() {
444 if retErr != nil {
Nigel Tao46077d32015-01-07 16:53:29 +1100445 h.LockSystem.Unlock(now, token)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100446 }
447 }()
Nigel Taobaf9fd42014-11-11 17:46:57 +1100448
449 // Create the resource if it didn't previously exist.
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200450 if _, err := h.FileSystem.Stat(reqPath); err != nil {
451 f, err := h.FileSystem.OpenFile(reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
Nigel Taobaf9fd42014-11-11 17:46:57 +1100452 if err != nil {
453 // TODO: detect missing intermediate dirs and return http.StatusConflict?
454 return http.StatusInternalServerError, err
455 }
456 f.Close()
Nigel Tao5feaa292015-02-03 17:29:08 +1100457 created = true
Nigel Taobaf9fd42014-11-11 17:46:57 +1100458 }
Nigel Tao2033b3a2015-01-30 16:26:12 +1100459
460 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
461 // Lock-Token value is a Coded-URL. We add angle brackets.
462 w.Header().Set("Lock-Token", "<"+token+">")
Nigel Taobaf9fd42014-11-11 17:46:57 +1100463 }
464
465 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
Nigel Tao5feaa292015-02-03 17:29:08 +1100466 if created {
467 // This is "w.WriteHeader(http.StatusCreated)" and not "return
468 // http.StatusCreated, nil" because we write our own (XML) response to w
469 // and Handler.ServeHTTP would otherwise write "Created".
470 w.WriteHeader(http.StatusCreated)
471 }
Nigel Taobaf9fd42014-11-11 17:46:57 +1100472 writeLockInfo(w, token, ld)
473 return 0, nil
474}
475
476func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {
477 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
478 // Lock-Token value is a Coded-URL. We strip its angle brackets.
479 t := r.Header.Get("Lock-Token")
480 if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
481 return http.StatusBadRequest, errInvalidLockToken
482 }
483 t = t[1 : len(t)-1]
484
Nigel Tao46077d32015-01-07 16:53:29 +1100485 switch err = h.LockSystem.Unlock(time.Now(), t); err {
Nigel Taobaf9fd42014-11-11 17:46:57 +1100486 case nil:
487 return http.StatusNoContent, err
488 case ErrForbidden:
489 return http.StatusForbidden, err
Nigel Tao46077d32015-01-07 16:53:29 +1100490 case ErrLocked:
491 return StatusLocked, err
Nigel Taobaf9fd42014-11-11 17:46:57 +1100492 case ErrNoSuchLock:
493 return http.StatusConflict, err
494 default:
495 return http.StatusInternalServerError, err
496 }
497}
498
Robert Stepanek7dbad502015-01-28 21:50:26 +0100499func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200500 reqPath, status, err := h.stripPrefix(r.URL.Path)
501 if err != nil {
502 return status, err
503 }
504 fi, err := h.FileSystem.Stat(reqPath)
Robert Stepanek7dbad502015-01-28 21:50:26 +0100505 if err != nil {
Robert Stepanek4dbd2a12015-05-26 12:32:18 +0200506 if os.IsNotExist(err) {
Robert Stepanek7dbad502015-01-28 21:50:26 +0100507 return http.StatusNotFound, err
508 }
509 return http.StatusMethodNotAllowed, err
510 }
511 depth := infiniteDepth
512 if hdr := r.Header.Get("Depth"); hdr != "" {
513 depth = parseDepth(hdr)
514 if depth == invalidDepth {
515 return http.StatusBadRequest, errInvalidDepth
516 }
517 }
518 pf, status, err := readPropfind(r.Body)
519 if err != nil {
520 return status, err
521 }
522
523 mw := multistatusWriter{w: w}
524
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200525 walkFn := func(reqPath string, info os.FileInfo, err error) error {
Robert Stepanek7dbad502015-01-28 21:50:26 +0100526 if err != nil {
527 return err
528 }
529 var pstats []Propstat
530 if pf.Propname != nil {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200531 pnames, err := propnames(h.FileSystem, h.LockSystem, reqPath)
Robert Stepanek7dbad502015-01-28 21:50:26 +0100532 if err != nil {
533 return err
534 }
535 pstat := Propstat{Status: http.StatusOK}
Nigel Taoad9eb392015-05-21 17:33:58 +1000536 for _, xmlname := range pnames {
Robert Stepanek7dbad502015-01-28 21:50:26 +0100537 pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
538 }
539 pstats = append(pstats, pstat)
540 } else if pf.Allprop != nil {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200541 pstats, err = allprop(h.FileSystem, h.LockSystem, reqPath, pf.Prop)
Robert Stepanek7dbad502015-01-28 21:50:26 +0100542 } else {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200543 pstats, err = props(h.FileSystem, h.LockSystem, reqPath, pf.Prop)
Robert Stepanek7dbad502015-01-28 21:50:26 +0100544 }
545 if err != nil {
546 return err
547 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200548 return mw.write(makePropstatResponse(path.Join(h.Prefix, reqPath), pstats))
Robert Stepanek7dbad502015-01-28 21:50:26 +0100549 }
550
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200551 walkErr := walkFS(h.FileSystem, depth, reqPath, fi, walkFn)
Robert Stepanekd1750812015-04-21 20:30:15 +0200552 closeErr := mw.close()
553 if walkErr != nil {
554 return http.StatusInternalServerError, walkErr
Robert Stepanek7dbad502015-01-28 21:50:26 +0100555 }
Robert Stepanekd1750812015-04-21 20:30:15 +0200556 if closeErr != nil {
557 return http.StatusInternalServerError, closeErr
Robert Stepanek7dbad502015-01-28 21:50:26 +0100558 }
Robert Stepanekd1750812015-04-21 20:30:15 +0200559 return 0, nil
Robert Stepanek7dbad502015-01-28 21:50:26 +0100560}
561
Robert Stepanek169f4222015-04-14 22:14:59 +0200562func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200563 reqPath, status, err := h.stripPrefix(r.URL.Path)
564 if err != nil {
565 return status, err
566 }
567 release, status, err := h.confirmLocks(r, reqPath, "")
Robert Stepanek169f4222015-04-14 22:14:59 +0200568 if err != nil {
569 return status, err
570 }
571 defer release()
572
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200573 if _, err := h.FileSystem.Stat(reqPath); err != nil {
Robert Stepanek4dbd2a12015-05-26 12:32:18 +0200574 if os.IsNotExist(err) {
Robert Stepanek169f4222015-04-14 22:14:59 +0200575 return http.StatusNotFound, err
576 }
577 return http.StatusMethodNotAllowed, err
578 }
579 patches, status, err := readProppatch(r.Body)
580 if err != nil {
581 return status, err
582 }
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200583 pstats, err := patch(h.FileSystem, h.LockSystem, reqPath, patches)
Robert Stepanek169f4222015-04-14 22:14:59 +0200584 if err != nil {
585 return http.StatusInternalServerError, err
586 }
587 mw := multistatusWriter{w: w}
588 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
589 closeErr := mw.close()
590 if writeErr != nil {
591 return http.StatusInternalServerError, writeErr
592 }
Robert Stepanekd1750812015-04-21 20:30:15 +0200593 if closeErr != nil {
594 return http.StatusInternalServerError, closeErr
595 }
596 return 0, nil
Robert Stepanek169f4222015-04-14 22:14:59 +0200597}
598
Robert Stepanek7dbad502015-01-28 21:50:26 +0100599func makePropstatResponse(href string, pstats []Propstat) *response {
600 resp := response{
Yasuhiro Matsumoto55cccaa2015-11-17 16:27:57 +0900601 Href: []string{(&url.URL{Path: href}).EscapedPath()},
Robert Stepanek7dbad502015-01-28 21:50:26 +0100602 Propstat: make([]propstat, 0, len(pstats)),
603 }
604 for _, p := range pstats {
605 var xmlErr *xmlError
606 if p.XMLError != "" {
607 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
608 }
609 resp.Propstat = append(resp.Propstat, propstat{
610 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
611 Prop: p.Props,
612 ResponseDescription: p.ResponseDescription,
613 Error: xmlErr,
614 })
615 }
616 return &resp
617}
618
Nigel Tao67f25492015-01-09 10:57:20 +1100619const (
620 infiniteDepth = -1
621 invalidDepth = -2
622)
623
624// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
625// infiniteDepth. Parsing any other string returns invalidDepth.
626//
627// Different WebDAV methods have further constraints on valid depths:
628// - PROPFIND has no further restrictions, as per section 9.1.
Nigel Taocd216762015-01-29 14:43:00 +1100629// - COPY accepts only "0" or "infinity", as per section 9.8.3.
630// - MOVE accepts only "infinity", as per section 9.9.2.
Nigel Tao67f25492015-01-09 10:57:20 +1100631// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
632// These constraints are enforced by the handleXxx methods.
633func parseDepth(s string) int {
634 switch s {
635 case "0":
636 return 0
637 case "1":
638 return 1
639 case "infinity":
640 return infiniteDepth
641 }
642 return invalidDepth
Nigel Taobaf9fd42014-11-11 17:46:57 +1100643}
644
Nigel Taobaf9fd42014-11-11 17:46:57 +1100645// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
646const (
647 StatusMulti = 207
648 StatusUnprocessableEntity = 422
649 StatusLocked = 423
650 StatusFailedDependency = 424
651 StatusInsufficientStorage = 507
652)
653
654func StatusText(code int) string {
655 switch code {
656 case StatusMulti:
657 return "Multi-Status"
658 case StatusUnprocessableEntity:
659 return "Unprocessable Entity"
660 case StatusLocked:
661 return "Locked"
662 case StatusFailedDependency:
663 return "Failed Dependency"
664 case StatusInsufficientStorage:
665 return "Insufficient Storage"
666 }
667 return http.StatusText(code)
668}
669
670var (
Nigel Taocd216762015-01-29 14:43:00 +1100671 errDestinationEqualsSource = errors.New("webdav: destination equals source")
672 errDirectoryNotEmpty = errors.New("webdav: directory not empty")
673 errInvalidDepth = errors.New("webdav: invalid depth")
674 errInvalidDestination = errors.New("webdav: invalid destination")
675 errInvalidIfHeader = errors.New("webdav: invalid If header")
676 errInvalidLockInfo = errors.New("webdav: invalid lock info")
677 errInvalidLockToken = errors.New("webdav: invalid lock token")
Nigel Taocd216762015-01-29 14:43:00 +1100678 errInvalidPropfind = errors.New("webdav: invalid propfind")
Robert Stepanek169f4222015-04-14 22:14:59 +0200679 errInvalidProppatch = errors.New("webdav: invalid proppatch")
Nigel Taocd216762015-01-29 14:43:00 +1100680 errInvalidResponse = errors.New("webdav: invalid response")
681 errInvalidTimeout = errors.New("webdav: invalid timeout")
682 errNoFileSystem = errors.New("webdav: no file system")
683 errNoLockSystem = errors.New("webdav: no lock system")
684 errNotADirectory = errors.New("webdav: not a directory")
Federico Simoncellidb8e4de2015-08-24 18:07:02 +0200685 errPrefixMismatch = errors.New("webdav: prefix mismatch")
Nigel Taocd216762015-01-29 14:43:00 +1100686 errRecursionTooDeep = errors.New("webdav: recursion too deep")
687 errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
Nigel Tao614fbbe2015-02-04 18:11:38 +1100688 errUnsupportedMethod = errors.New("webdav: unsupported method")
Nigel Taobaf9fd42014-11-11 17:46:57 +1100689)