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 return http.StatusNotFound, err
271 }
272 _, copyErr := io.Copy(f, r.Body)
273 fi, statErr := f.Stat()
274 closeErr := f.Close()
275
276 if copyErr != nil {
277 return http.StatusMethodNotAllowed, copyErr
278 }
279 if statErr != nil {
280 return http.StatusMethodNotAllowed, statErr
281 }
282 if closeErr != nil {
283 return http.StatusMethodNotAllowed, closeErr
284 }
285 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
286 if err != nil {
287 return http.StatusInternalServerError, err
288 }
289 w.Header().Set("ETag", etag)
290 return http.StatusCreated, nil
291 }
292
293 func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
294 reqPath, status, err := h.stripPrefix(r.URL.Path)
295 if err != nil {
296 return status, err
297 }
298 release, status, err := h.confirmLocks(r, reqPath, "")
299 if err != nil {
300 return status, err
301 }
302 defer release()
303
304 ctx := r.Context()
305
306 if r.ContentLength > 0 {
307 return http.StatusUnsupportedMediaType, nil
308 }
309 if err := h.FileSystem.Mkdir(ctx, reqPath, 0777); err != nil {
310 if os.IsNotExist(err) {
311 return http.StatusConflict, err
312 }
313 return http.StatusMethodNotAllowed, err
314 }
315 return http.StatusCreated, nil
316 }
317
318 func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
319 hdr := r.Header.Get("Destination")
320 if hdr == "" {
321 return http.StatusBadRequest, errInvalidDestination
322 }
323 u, err := url.Parse(hdr)
324 if err != nil {
325 return http.StatusBadRequest, errInvalidDestination
326 }
327 if u.Host != "" && u.Host != r.Host {
328 return http.StatusBadGateway, errInvalidDestination
329 }
330
331 src, status, err := h.stripPrefix(r.URL.Path)
332 if err != nil {
333 return status, err
334 }
335
336 dst, status, err := h.stripPrefix(u.Path)
337 if err != nil {
338 return status, err
339 }
340
341 if dst == "" {
342 return http.StatusBadGateway, errInvalidDestination
343 }
344 if dst == src {
345 return http.StatusForbidden, errDestinationEqualsSource
346 }
347
348 ctx := r.Context()
349
350 if r.Method == "COPY" {
351
352
353
354
355
356 release, status, err := h.confirmLocks(r, "", dst)
357 if err != nil {
358 return status, err
359 }
360 defer release()
361
362
363
364 depth := infiniteDepth
365 if hdr := r.Header.Get("Depth"); hdr != "" {
366 depth = parseDepth(hdr)
367 if depth != 0 && depth != infiniteDepth {
368
369
370 return http.StatusBadRequest, errInvalidDepth
371 }
372 }
373 return copyFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
374 }
375
376 release, status, err := h.confirmLocks(r, src, dst)
377 if err != nil {
378 return status, err
379 }
380 defer release()
381
382
383
384
385 if hdr := r.Header.Get("Depth"); hdr != "" {
386 if parseDepth(hdr) != infiniteDepth {
387 return http.StatusBadRequest, errInvalidDepth
388 }
389 }
390 return moveFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T")
391 }
392
393 func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
394 duration, err := parseTimeout(r.Header.Get("Timeout"))
395 if err != nil {
396 return http.StatusBadRequest, err
397 }
398 li, status, err := readLockInfo(r.Body)
399 if err != nil {
400 return status, err
401 }
402
403 ctx := r.Context()
404 token, ld, now, created := "", LockDetails{}, time.Now(), false
405 if li == (lockInfo{}) {
406
407 ih, ok := parseIfHeader(r.Header.Get("If"))
408 if !ok {
409 return http.StatusBadRequest, errInvalidIfHeader
410 }
411 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
412 token = ih.lists[0].conditions[0].Token
413 }
414 if token == "" {
415 return http.StatusBadRequest, errInvalidLockToken
416 }
417 ld, err = h.LockSystem.Refresh(now, token, duration)
418 if err != nil {
419 if err == ErrNoSuchLock {
420 return http.StatusPreconditionFailed, err
421 }
422 return http.StatusInternalServerError, err
423 }
424
425 } else {
426
427
428 depth := infiniteDepth
429 if hdr := r.Header.Get("Depth"); hdr != "" {
430 depth = parseDepth(hdr)
431 if depth != 0 && depth != infiniteDepth {
432
433
434 return http.StatusBadRequest, errInvalidDepth
435 }
436 }
437 reqPath, status, err := h.stripPrefix(r.URL.Path)
438 if err != nil {
439 return status, err
440 }
441 ld = LockDetails{
442 Root: reqPath,
443 Duration: duration,
444 OwnerXML: li.Owner.InnerXML,
445 ZeroDepth: depth == 0,
446 }
447 token, err = h.LockSystem.Create(now, ld)
448 if err != nil {
449 if err == ErrLocked {
450 return StatusLocked, err
451 }
452 return http.StatusInternalServerError, err
453 }
454 defer func() {
455 if retErr != nil {
456 h.LockSystem.Unlock(now, token)
457 }
458 }()
459
460
461 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
462 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
463 if err != nil {
464
465 return http.StatusInternalServerError, err
466 }
467 f.Close()
468 created = true
469 }
470
471
472
473 w.Header().Set("Lock-Token", "<"+token+">")
474 }
475
476 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
477 if created {
478
479
480
481 w.WriteHeader(http.StatusCreated)
482 }
483 writeLockInfo(w, token, ld)
484 return 0, nil
485 }
486
487 func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {
488
489
490 t := r.Header.Get("Lock-Token")
491 if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
492 return http.StatusBadRequest, errInvalidLockToken
493 }
494 t = t[1 : len(t)-1]
495
496 switch err = h.LockSystem.Unlock(time.Now(), t); err {
497 case nil:
498 return http.StatusNoContent, err
499 case ErrForbidden:
500 return http.StatusForbidden, err
501 case ErrLocked:
502 return StatusLocked, err
503 case ErrNoSuchLock:
504 return http.StatusConflict, err
505 default:
506 return http.StatusInternalServerError, err
507 }
508 }
509
510 func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) {
511 reqPath, status, err := h.stripPrefix(r.URL.Path)
512 if err != nil {
513 return status, err
514 }
515 ctx := r.Context()
516 fi, err := h.FileSystem.Stat(ctx, reqPath)
517 if err != nil {
518 if os.IsNotExist(err) {
519 return http.StatusNotFound, err
520 }
521 return http.StatusMethodNotAllowed, err
522 }
523 depth := infiniteDepth
524 if hdr := r.Header.Get("Depth"); hdr != "" {
525 depth = parseDepth(hdr)
526 if depth == invalidDepth {
527 return http.StatusBadRequest, errInvalidDepth
528 }
529 }
530 pf, status, err := readPropfind(r.Body)
531 if err != nil {
532 return status, err
533 }
534
535 mw := multistatusWriter{w: w}
536
537 walkFn := func(reqPath string, info os.FileInfo, err error) error {
538 if err != nil {
539 return handlePropfindError(err, info)
540 }
541
542 var pstats []Propstat
543 if pf.Propname != nil {
544 pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath)
545 if err != nil {
546 return handlePropfindError(err, info)
547 }
548 pstat := Propstat{Status: http.StatusOK}
549 for _, xmlname := range pnames {
550 pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
551 }
552 pstats = append(pstats, pstat)
553 } else if pf.Allprop != nil {
554 pstats, err = allprop(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
555 } else {
556 pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
557 }
558 if err != nil {
559 return handlePropfindError(err, info)
560 }
561 href := path.Join(h.Prefix, reqPath)
562 if href != "/" && info.IsDir() {
563 href += "/"
564 }
565 return mw.write(makePropstatResponse(href, pstats))
566 }
567
568 walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn)
569 closeErr := mw.close()
570 if walkErr != nil {
571 return http.StatusInternalServerError, walkErr
572 }
573 if closeErr != nil {
574 return http.StatusInternalServerError, closeErr
575 }
576 return 0, nil
577 }
578
579 func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
580 reqPath, status, err := h.stripPrefix(r.URL.Path)
581 if err != nil {
582 return status, err
583 }
584 release, status, err := h.confirmLocks(r, reqPath, "")
585 if err != nil {
586 return status, err
587 }
588 defer release()
589
590 ctx := r.Context()
591
592 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
593 if os.IsNotExist(err) {
594 return http.StatusNotFound, err
595 }
596 return http.StatusMethodNotAllowed, err
597 }
598 patches, status, err := readProppatch(r.Body)
599 if err != nil {
600 return status, err
601 }
602 pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches)
603 if err != nil {
604 return http.StatusInternalServerError, err
605 }
606 mw := multistatusWriter{w: w}
607 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
608 closeErr := mw.close()
609 if writeErr != nil {
610 return http.StatusInternalServerError, writeErr
611 }
612 if closeErr != nil {
613 return http.StatusInternalServerError, closeErr
614 }
615 return 0, nil
616 }
617
618 func makePropstatResponse(href string, pstats []Propstat) *response {
619 resp := response{
620 Href: []string{(&url.URL{Path: href}).EscapedPath()},
621 Propstat: make([]propstat, 0, len(pstats)),
622 }
623 for _, p := range pstats {
624 var xmlErr *xmlError
625 if p.XMLError != "" {
626 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
627 }
628 resp.Propstat = append(resp.Propstat, propstat{
629 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
630 Prop: p.Props,
631 ResponseDescription: p.ResponseDescription,
632 Error: xmlErr,
633 })
634 }
635 return &resp
636 }
637
638 func handlePropfindError(err error, info os.FileInfo) error {
639 var skipResp error = nil
640 if info != nil && info.IsDir() {
641 skipResp = filepath.SkipDir
642 }
643
644 if errors.Is(err, os.ErrPermission) {
645
646
647 return skipResp
648 }
649
650 if _, ok := err.(*os.PathError); ok {
651
652 return skipResp
653 }
654
655
656
657
658
659
660
661
662 return err
663 }
664
665 const (
666 infiniteDepth = -1
667 invalidDepth = -2
668 )
669
670
671
672
673
674
675
676
677
678
679
680 func parseDepth(s string) int {
681 switch s {
682 case "0":
683 return 0
684 case "1":
685 return 1
686 case "infinity":
687 return infiniteDepth
688 }
689 return invalidDepth
690 }
691
692
693 const (
694 StatusMulti = 207
695 StatusUnprocessableEntity = 422
696 StatusLocked = 423
697 StatusFailedDependency = 424
698 StatusInsufficientStorage = 507
699 )
700
701 func StatusText(code int) string {
702 switch code {
703 case StatusMulti:
704 return "Multi-Status"
705 case StatusUnprocessableEntity:
706 return "Unprocessable Entity"
707 case StatusLocked:
708 return "Locked"
709 case StatusFailedDependency:
710 return "Failed Dependency"
711 case StatusInsufficientStorage:
712 return "Insufficient Storage"
713 }
714 return http.StatusText(code)
715 }
716
717 var (
718 errDestinationEqualsSource = errors.New("webdav: destination equals source")
719 errDirectoryNotEmpty = errors.New("webdav: directory not empty")
720 errInvalidDepth = errors.New("webdav: invalid depth")
721 errInvalidDestination = errors.New("webdav: invalid destination")
722 errInvalidIfHeader = errors.New("webdav: invalid If header")
723 errInvalidLockInfo = errors.New("webdav: invalid lock info")
724 errInvalidLockToken = errors.New("webdav: invalid lock token")
725 errInvalidPropfind = errors.New("webdav: invalid propfind")
726 errInvalidProppatch = errors.New("webdav: invalid proppatch")
727 errInvalidResponse = errors.New("webdav: invalid response")
728 errInvalidTimeout = errors.New("webdav: invalid timeout")
729 errNoFileSystem = errors.New("webdav: no file system")
730 errNoLockSystem = errors.New("webdav: no lock system")
731 errNotADirectory = errors.New("webdav: not a directory")
732 errPrefixMismatch = errors.New("webdav: prefix mismatch")
733 errRecursionTooDeep = errors.New("webdav: recursion too deep")
734 errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
735 errUnsupportedMethod = errors.New("webdav: unsupported method")
736 )
737
View as plain text