1
2
3
4
5
6 package webdav
7
8 import (
9 "errors"
10 "fmt"
11 "io"
12 "net/http"
13 "net/url"
14 "os"
15 "path"
16 "path/filepath"
17 "strings"
18 "time"
19 )
20
21 type Handler struct {
22
23 Prefix string
24
25 FileSystem FileSystem
26
27 LockSystem LockSystem
28
29
30 Logger func(*http.Request, error)
31 }
32
33 func (h *Handler) stripPrefix(p string) (string, int, error) {
34 if h.Prefix == "" {
35 return p, http.StatusOK, nil
36 }
37 if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) {
38 return r, http.StatusOK, nil
39 }
40 return p, http.StatusNotFound, errPrefixMismatch
41 }
42
43 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
44 status, err := http.StatusBadRequest, errUnsupportedMethod
45 if h.FileSystem == nil {
46 status, err = http.StatusInternalServerError, errNoFileSystem
47 } else if h.LockSystem == nil {
48 status, err = http.StatusInternalServerError, errNoLockSystem
49 } else {
50 switch r.Method {
51 case "OPTIONS":
52 status, err = h.handleOptions(w, r)
53 case "GET", "HEAD", "POST":
54 status, err = h.handleGetHeadPost(w, r)
55 case "DELETE":
56 status, err = h.handleDelete(w, r)
57 case "PUT":
58 status, err = h.handlePut(w, r)
59 case "MKCOL":
60 status, err = h.handleMkcol(w, r)
61 case "COPY", "MOVE":
62 status, err = h.handleCopyMove(w, r)
63 case "LOCK":
64 status, err = h.handleLock(w, r)
65 case "UNLOCK":
66 status, err = h.handleUnlock(w, r)
67 case "PROPFIND":
68 status, err = h.handlePropfind(w, r)
69 case "PROPPATCH":
70 status, err = h.handleProppatch(w, r)
71 }
72 }
73
74 if status != 0 {
75 w.WriteHeader(status)
76 if status != http.StatusNoContent {
77 w.Write([]byte(StatusText(status)))
78 }
79 }
80 if h.Logger != nil {
81 h.Logger(r, err)
82 }
83 }
84
85 func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) {
86 token, err = h.LockSystem.Create(now, LockDetails{
87 Root: root,
88 Duration: infiniteTimeout,
89 ZeroDepth: true,
90 })
91 if err != nil {
92 if err == ErrLocked {
93 return "", StatusLocked, err
94 }
95 return "", http.StatusInternalServerError, err
96 }
97 return token, 0, nil
98 }
99
100 func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {
101 hdr := r.Header.Get("If")
102 if hdr == "" {
103
104
105
106
107
108 now, srcToken, dstToken := time.Now(), "", ""
109 if src != "" {
110 srcToken, status, err = h.lock(now, src)
111 if err != nil {
112 return nil, status, err
113 }
114 }
115 if dst != "" {
116 dstToken, status, err = h.lock(now, dst)
117 if err != nil {
118 if srcToken != "" {
119 h.LockSystem.Unlock(now, srcToken)
120 }
121 return nil, status, err
122 }
123 }
124
125 return func() {
126 if dstToken != "" {
127 h.LockSystem.Unlock(now, dstToken)
128 }
129 if srcToken != "" {
130 h.LockSystem.Unlock(now, srcToken)
131 }
132 }, 0, nil
133 }
134
135 ih, ok := parseIfHeader(hdr)
136 if !ok {
137 return nil, http.StatusBadRequest, errInvalidIfHeader
138 }
139
140 for _, l := range ih.lists {
141 lsrc := l.resourceTag
142 if lsrc == "" {
143 lsrc = src
144 } else {
145 u, err := url.Parse(lsrc)
146 if err != nil {
147 continue
148 }
149 if u.Host != r.Host {
150 continue
151 }
152 lsrc, status, err = h.stripPrefix(u.Path)
153 if err != nil {
154 return nil, status, err
155 }
156 }
157 release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...)
158 if err == ErrConfirmationFailed {
159 continue
160 }
161 if err != nil {
162 return nil, http.StatusInternalServerError, err
163 }
164 return release, 0, nil
165 }
166
167
168
169
170 return nil, http.StatusPreconditionFailed, ErrLocked
171 }
172
173 func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) {
174 reqPath, status, err := h.stripPrefix(r.URL.Path)
175 if err != nil {
176 return status, err
177 }
178 ctx := r.Context()
179 allow := "OPTIONS, LOCK, PUT, MKCOL"
180 if fi, err := h.FileSystem.Stat(ctx, reqPath); err == nil {
181 if fi.IsDir() {
182 allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND"
183 } else {
184 allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"
185 }
186 }
187 w.Header().Set("Allow", allow)
188
189 w.Header().Set("DAV", "1, 2")
190
191 w.Header().Set("MS-Author-Via", "DAV")
192 return 0, nil
193 }
194
195 func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
196 reqPath, status, err := h.stripPrefix(r.URL.Path)
197 if err != nil {
198 return status, err
199 }
200
201 ctx := r.Context()
202 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDONLY, 0)
203 if err != nil {
204 return http.StatusNotFound, err
205 }
206 defer f.Close()
207 fi, err := f.Stat()
208 if err != nil {
209 return http.StatusNotFound, err
210 }
211 if fi.IsDir() {
212 return http.StatusMethodNotAllowed, nil
213 }
214 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
215 if err != nil {
216 return http.StatusInternalServerError, err
217 }
218 w.Header().Set("ETag", etag)
219
220 http.ServeContent(w, r, reqPath, fi.ModTime(), f)
221 return 0, nil
222 }
223
224 func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
225 reqPath, status, err := h.stripPrefix(r.URL.Path)
226 if err != nil {
227 return status, err
228 }
229 release, status, err := h.confirmLocks(r, reqPath, "")
230 if err != nil {
231 return status, err
232 }
233 defer release()
234
235 ctx := r.Context()
236
237
238
239
240
241
242 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
243 if os.IsNotExist(err) {
244 return http.StatusNotFound, err
245 }
246 return http.StatusMethodNotAllowed, err
247 }
248 if err := h.FileSystem.RemoveAll(ctx, reqPath); err != nil {
249 return http.StatusMethodNotAllowed, err
250 }
251 return http.StatusNoContent, nil
252 }
253
254 func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
255 reqPath, status, err := h.stripPrefix(r.URL.Path)
256 if err != nil {
257 return status, err
258 }
259 release, status, err := h.confirmLocks(r, reqPath, "")
260 if err != nil {
261 return status, err
262 }
263 defer release()
264
265
266 ctx := r.Context()
267
268 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
269 if err != nil {
270 if os.IsNotExist(err) {
271 return http.StatusConflict, err
272 }
273 return http.StatusNotFound, err
274 }
275 _, copyErr := io.Copy(f, r.Body)
276 fi, statErr := f.Stat()
277 closeErr := f.Close()
278
279 if copyErr != nil {
280 return http.StatusMethodNotAllowed, copyErr
281 }
282 if statErr != nil {
283 return http.StatusMethodNotAllowed, statErr
284 }
285 if closeErr != nil {
286 return http.StatusMethodNotAllowed, closeErr
287 }
288 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
289 if err != nil {
290 return http.StatusInternalServerError, err
291 }
292 w.Header().Set("ETag", etag)
293 return http.StatusCreated, nil
294 }
295
296 func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
297 reqPath, status, err := h.stripPrefix(r.URL.Path)
298 if err != nil {
299 return status, err
300 }
301 release, status, err := h.confirmLocks(r, reqPath, "")
302 if err != nil {
303 return status, err
304 }
305 defer release()
306
307 ctx := r.Context()
308
309 if r.ContentLength > 0 {
310 return http.StatusUnsupportedMediaType, nil
311 }
312 if err := h.FileSystem.Mkdir(ctx, reqPath, 0777); err != nil {
313 if os.IsNotExist(err) {
314 return http.StatusConflict, err
315 }
316 return http.StatusMethodNotAllowed, err
317 }
318 return http.StatusCreated, nil
319 }
320
321 func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
322 hdr := r.Header.Get("Destination")
323 if hdr == "" {
324 return http.StatusBadRequest, errInvalidDestination
325 }
326 u, err := url.Parse(hdr)
327 if err != nil {
328 return http.StatusBadRequest, errInvalidDestination
329 }
330 if u.Host != "" && u.Host != r.Host {
331 return http.StatusBadGateway, errInvalidDestination
332 }
333
334 src, status, err := h.stripPrefix(r.URL.Path)
335 if err != nil {
336 return status, err
337 }
338
339 dst, status, err := h.stripPrefix(u.Path)
340 if err != nil {
341 return status, err
342 }
343
344 if dst == "" {
345 return http.StatusBadGateway, errInvalidDestination
346 }
347 if dst == src {
348 return http.StatusForbidden, errDestinationEqualsSource
349 }
350
351 ctx := r.Context()
352
353 if r.Method == "COPY" {
354
355
356
357
358
359 release, status, err := h.confirmLocks(r, "", dst)
360 if err != nil {
361 return status, err
362 }
363 defer release()
364
365
366
367 depth := infiniteDepth
368 if hdr := r.Header.Get("Depth"); hdr != "" {
369 depth = parseDepth(hdr)
370 if depth != 0 && depth != infiniteDepth {
371
372
373 return http.StatusBadRequest, errInvalidDepth
374 }
375 }
376 return copyFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
377 }
378
379 release, status, err := h.confirmLocks(r, src, dst)
380 if err != nil {
381 return status, err
382 }
383 defer release()
384
385
386
387
388 if hdr := r.Header.Get("Depth"); hdr != "" {
389 if parseDepth(hdr) != infiniteDepth {
390 return http.StatusBadRequest, errInvalidDepth
391 }
392 }
393 return moveFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T")
394 }
395
396 func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
397 duration, err := parseTimeout(r.Header.Get("Timeout"))
398 if err != nil {
399 return http.StatusBadRequest, err
400 }
401 li, status, err := readLockInfo(r.Body)
402 if err != nil {
403 return status, err
404 }
405
406 ctx := r.Context()
407 token, ld, now, created := "", LockDetails{}, time.Now(), false
408 if li == (lockInfo{}) {
409
410 ih, ok := parseIfHeader(r.Header.Get("If"))
411 if !ok {
412 return http.StatusBadRequest, errInvalidIfHeader
413 }
414 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
415 token = ih.lists[0].conditions[0].Token
416 }
417 if token == "" {
418 return http.StatusBadRequest, errInvalidLockToken
419 }
420 ld, err = h.LockSystem.Refresh(now, token, duration)
421 if err != nil {
422 if err == ErrNoSuchLock {
423 return http.StatusPreconditionFailed, err
424 }
425 return http.StatusInternalServerError, err
426 }
427
428 } else {
429
430
431 depth := infiniteDepth
432 if hdr := r.Header.Get("Depth"); hdr != "" {
433 depth = parseDepth(hdr)
434 if depth != 0 && depth != infiniteDepth {
435
436
437 return http.StatusBadRequest, errInvalidDepth
438 }
439 }
440 reqPath, status, err := h.stripPrefix(r.URL.Path)
441 if err != nil {
442 return status, err
443 }
444 ld = LockDetails{
445 Root: reqPath,
446 Duration: duration,
447 OwnerXML: li.Owner.InnerXML,
448 ZeroDepth: depth == 0,
449 }
450 token, err = h.LockSystem.Create(now, ld)
451 if err != nil {
452 if err == ErrLocked {
453 return StatusLocked, err
454 }
455 return http.StatusInternalServerError, err
456 }
457 defer func() {
458 if retErr != nil {
459 h.LockSystem.Unlock(now, token)
460 }
461 }()
462
463
464 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
465 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
466 if err != nil {
467
468 return http.StatusInternalServerError, err
469 }
470 f.Close()
471 created = true
472 }
473
474
475
476 w.Header().Set("Lock-Token", "<"+token+">")
477 }
478
479 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
480 if created {
481
482
483
484 w.WriteHeader(http.StatusCreated)
485 }
486 writeLockInfo(w, token, ld)
487 return 0, nil
488 }
489
490 func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {
491
492
493 t := r.Header.Get("Lock-Token")
494 if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
495 return http.StatusBadRequest, errInvalidLockToken
496 }
497 t = t[1 : len(t)-1]
498
499 switch err = h.LockSystem.Unlock(time.Now(), t); err {
500 case nil:
501 return http.StatusNoContent, err
502 case ErrForbidden:
503 return http.StatusForbidden, err
504 case ErrLocked:
505 return StatusLocked, err
506 case ErrNoSuchLock:
507 return http.StatusConflict, err
508 default:
509 return http.StatusInternalServerError, err
510 }
511 }
512
513 func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) {
514 reqPath, status, err := h.stripPrefix(r.URL.Path)
515 if err != nil {
516 return status, err
517 }
518 ctx := r.Context()
519 fi, err := h.FileSystem.Stat(ctx, reqPath)
520 if err != nil {
521 if os.IsNotExist(err) {
522 return http.StatusNotFound, err
523 }
524 return http.StatusMethodNotAllowed, err
525 }
526 depth := infiniteDepth
527 if hdr := r.Header.Get("Depth"); hdr != "" {
528 depth = parseDepth(hdr)
529 if depth == invalidDepth {
530 return http.StatusBadRequest, errInvalidDepth
531 }
532 }
533 pf, status, err := readPropfind(r.Body)
534 if err != nil {
535 return status, err
536 }
537
538 mw := multistatusWriter{w: w}
539
540 walkFn := func(reqPath string, info os.FileInfo, err error) error {
541 if err != nil {
542 return handlePropfindError(err, info)
543 }
544
545 var pstats []Propstat
546 if pf.Propname != nil {
547 pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath)
548 if err != nil {
549 return handlePropfindError(err, info)
550 }
551 pstat := Propstat{Status: http.StatusOK}
552 for _, xmlname := range pnames {
553 pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
554 }
555 pstats = append(pstats, pstat)
556 } else if pf.Allprop != nil {
557 pstats, err = allprop(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
558 } else {
559 pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
560 }
561 if err != nil {
562 return handlePropfindError(err, info)
563 }
564 href := path.Join(h.Prefix, reqPath)
565 if href != "/" && info.IsDir() {
566 href += "/"
567 }
568 return mw.write(makePropstatResponse(href, pstats))
569 }
570
571 walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn)
572 closeErr := mw.close()
573 if walkErr != nil {
574 return http.StatusInternalServerError, walkErr
575 }
576 if closeErr != nil {
577 return http.StatusInternalServerError, closeErr
578 }
579 return 0, nil
580 }
581
582 func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
583 reqPath, status, err := h.stripPrefix(r.URL.Path)
584 if err != nil {
585 return status, err
586 }
587 release, status, err := h.confirmLocks(r, reqPath, "")
588 if err != nil {
589 return status, err
590 }
591 defer release()
592
593 ctx := r.Context()
594
595 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
596 if os.IsNotExist(err) {
597 return http.StatusNotFound, err
598 }
599 return http.StatusMethodNotAllowed, err
600 }
601 patches, status, err := readProppatch(r.Body)
602 if err != nil {
603 return status, err
604 }
605 pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches)
606 if err != nil {
607 return http.StatusInternalServerError, err
608 }
609 mw := multistatusWriter{w: w}
610 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
611 closeErr := mw.close()
612 if writeErr != nil {
613 return http.StatusInternalServerError, writeErr
614 }
615 if closeErr != nil {
616 return http.StatusInternalServerError, closeErr
617 }
618 return 0, nil
619 }
620
621 func makePropstatResponse(href string, pstats []Propstat) *response {
622 resp := response{
623 Href: []string{(&url.URL{Path: href}).EscapedPath()},
624 Propstat: make([]propstat, 0, len(pstats)),
625 }
626 for _, p := range pstats {
627 var xmlErr *xmlError
628 if p.XMLError != "" {
629 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
630 }
631 resp.Propstat = append(resp.Propstat, propstat{
632 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
633 Prop: p.Props,
634 ResponseDescription: p.ResponseDescription,
635 Error: xmlErr,
636 })
637 }
638 return &resp
639 }
640
641 func handlePropfindError(err error, info os.FileInfo) error {
642 var skipResp error = nil
643 if info != nil && info.IsDir() {
644 skipResp = filepath.SkipDir
645 }
646
647 if errors.Is(err, os.ErrPermission) {
648
649
650 return skipResp
651 }
652
653 if _, ok := err.(*os.PathError); ok {
654
655 return skipResp
656 }
657
658
659
660
661
662
663
664
665 return err
666 }
667
668 const (
669 infiniteDepth = -1
670 invalidDepth = -2
671 )
672
673
674
675
676
677
678
679
680
681
682
683 func parseDepth(s string) int {
684 switch s {
685 case "0":
686 return 0
687 case "1":
688 return 1
689 case "infinity":
690 return infiniteDepth
691 }
692 return invalidDepth
693 }
694
695
696 const (
697 StatusMulti = 207
698 StatusUnprocessableEntity = 422
699 StatusLocked = 423
700 StatusFailedDependency = 424
701 StatusInsufficientStorage = 507
702 )
703
704 func StatusText(code int) string {
705 switch code {
706 case StatusMulti:
707 return "Multi-Status"
708 case StatusUnprocessableEntity:
709 return "Unprocessable Entity"
710 case StatusLocked:
711 return "Locked"
712 case StatusFailedDependency:
713 return "Failed Dependency"
714 case StatusInsufficientStorage:
715 return "Insufficient Storage"
716 }
717 return http.StatusText(code)
718 }
719
720 var (
721 errDestinationEqualsSource = errors.New("webdav: destination equals source")
722 errDirectoryNotEmpty = errors.New("webdav: directory not empty")
723 errInvalidDepth = errors.New("webdav: invalid depth")
724 errInvalidDestination = errors.New("webdav: invalid destination")
725 errInvalidIfHeader = errors.New("webdav: invalid If header")
726 errInvalidLockInfo = errors.New("webdav: invalid lock info")
727 errInvalidLockToken = errors.New("webdav: invalid lock token")
728 errInvalidPropfind = errors.New("webdav: invalid propfind")
729 errInvalidProppatch = errors.New("webdav: invalid proppatch")
730 errInvalidResponse = errors.New("webdav: invalid response")
731 errInvalidTimeout = errors.New("webdav: invalid timeout")
732 errNoFileSystem = errors.New("webdav: no file system")
733 errNoLockSystem = errors.New("webdav: no lock system")
734 errNotADirectory = errors.New("webdav: not a directory")
735 errPrefixMismatch = errors.New("webdav: prefix mismatch")
736 errRecursionTooDeep = errors.New("webdav: recursion too deep")
737 errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
738 errUnsupportedMethod = errors.New("webdav: unsupported method")
739 )
740
View as plain text