1
2
3
4
5 package webdav
6
7 import (
8 "bytes"
9 "context"
10 "encoding/xml"
11 "errors"
12 "fmt"
13 "io"
14 "mime"
15 "net/http"
16 "os"
17 "path/filepath"
18 "strconv"
19 )
20
21
22
23 type Proppatch struct {
24
25
26 Remove bool
27
28 Props []Property
29 }
30
31
32
33 type Propstat struct {
34
35 Props []Property
36
37
38
39
40
41 Status int
42
43
44
45
46
47 XMLError string
48
49
50
51 ResponseDescription string
52 }
53
54
55
56
57 func makePropstats(x, y Propstat) []Propstat {
58 pstats := make([]Propstat, 0, 2)
59 if len(x.Props) != 0 {
60 pstats = append(pstats, x)
61 }
62 if len(y.Props) != 0 {
63 pstats = append(pstats, y)
64 }
65 if len(pstats) == 0 {
66 pstats = append(pstats, Propstat{
67 Status: http.StatusOK,
68 })
69 }
70 return pstats
71 }
72
73
74
75
76
77
78
79
80
81
82
83
84 type DeadPropsHolder interface {
85
86 DeadProps() (map[xml.Name]Property, error)
87
88
89
90
91
92
93
94
95
96
97
98
99 Patch([]Proppatch) ([]Propstat, error)
100 }
101
102
103 var liveProps = map[xml.Name]struct {
104
105
106 findFn func(context.Context, FileSystem, LockSystem, string, os.FileInfo) (string, error)
107
108 dir bool
109 }{
110 {Space: "DAV:", Local: "resourcetype"}: {
111 findFn: findResourceType,
112 dir: true,
113 },
114 {Space: "DAV:", Local: "displayname"}: {
115 findFn: findDisplayName,
116 dir: true,
117 },
118 {Space: "DAV:", Local: "getcontentlength"}: {
119 findFn: findContentLength,
120 dir: false,
121 },
122 {Space: "DAV:", Local: "getlastmodified"}: {
123 findFn: findLastModified,
124
125
126
127
128
129
130
131 dir: true,
132 },
133 {Space: "DAV:", Local: "creationdate"}: {
134 findFn: nil,
135 dir: false,
136 },
137 {Space: "DAV:", Local: "getcontentlanguage"}: {
138 findFn: nil,
139 dir: false,
140 },
141 {Space: "DAV:", Local: "getcontenttype"}: {
142 findFn: findContentType,
143 dir: false,
144 },
145 {Space: "DAV:", Local: "getetag"}: {
146 findFn: findETag,
147
148
149
150
151 dir: false,
152 },
153
154
155
156 {Space: "DAV:", Local: "lockdiscovery"}: {},
157 {Space: "DAV:", Local: "supportedlock"}: {
158 findFn: findSupportedLock,
159 dir: true,
160 },
161 }
162
163
164
165
166
167
168
169 func props(ctx context.Context, fs FileSystem, ls LockSystem, name string, pnames []xml.Name) ([]Propstat, error) {
170 f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
171 if err != nil {
172 return nil, err
173 }
174 defer f.Close()
175 fi, err := f.Stat()
176 if err != nil {
177 return nil, err
178 }
179 isDir := fi.IsDir()
180
181 var deadProps map[xml.Name]Property
182 if dph, ok := f.(DeadPropsHolder); ok {
183 deadProps, err = dph.DeadProps()
184 if err != nil {
185 return nil, err
186 }
187 }
188
189 pstatOK := Propstat{Status: http.StatusOK}
190 pstatNotFound := Propstat{Status: http.StatusNotFound}
191 for _, pn := range pnames {
192
193 if dp, ok := deadProps[pn]; ok {
194 pstatOK.Props = append(pstatOK.Props, dp)
195 continue
196 }
197
198 if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
199 innerXML, err := prop.findFn(ctx, fs, ls, name, fi)
200 if err != nil {
201 return nil, err
202 }
203 pstatOK.Props = append(pstatOK.Props, Property{
204 XMLName: pn,
205 InnerXML: []byte(innerXML),
206 })
207 } else {
208 pstatNotFound.Props = append(pstatNotFound.Props, Property{
209 XMLName: pn,
210 })
211 }
212 }
213 return makePropstats(pstatOK, pstatNotFound), nil
214 }
215
216
217 func propnames(ctx context.Context, fs FileSystem, ls LockSystem, name string) ([]xml.Name, error) {
218 f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
219 if err != nil {
220 return nil, err
221 }
222 defer f.Close()
223 fi, err := f.Stat()
224 if err != nil {
225 return nil, err
226 }
227 isDir := fi.IsDir()
228
229 var deadProps map[xml.Name]Property
230 if dph, ok := f.(DeadPropsHolder); ok {
231 deadProps, err = dph.DeadProps()
232 if err != nil {
233 return nil, err
234 }
235 }
236
237 pnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
238 for pn, prop := range liveProps {
239 if prop.findFn != nil && (prop.dir || !isDir) {
240 pnames = append(pnames, pn)
241 }
242 }
243 for pn := range deadProps {
244 pnames = append(pnames, pn)
245 }
246 return pnames, nil
247 }
248
249
250
251
252
253
254
255
256
257 func allprop(ctx context.Context, fs FileSystem, ls LockSystem, name string, include []xml.Name) ([]Propstat, error) {
258 pnames, err := propnames(ctx, fs, ls, name)
259 if err != nil {
260 return nil, err
261 }
262
263 nameset := make(map[xml.Name]bool)
264 for _, pn := range pnames {
265 nameset[pn] = true
266 }
267 for _, pn := range include {
268 if !nameset[pn] {
269 pnames = append(pnames, pn)
270 }
271 }
272 return props(ctx, fs, ls, name, pnames)
273 }
274
275
276
277 func patch(ctx context.Context, fs FileSystem, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
278 conflict := false
279 loop:
280 for _, patch := range patches {
281 for _, p := range patch.Props {
282 if _, ok := liveProps[p.XMLName]; ok {
283 conflict = true
284 break loop
285 }
286 }
287 }
288 if conflict {
289 pstatForbidden := Propstat{
290 Status: http.StatusForbidden,
291 XMLError: `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`,
292 }
293 pstatFailedDep := Propstat{
294 Status: StatusFailedDependency,
295 }
296 for _, patch := range patches {
297 for _, p := range patch.Props {
298 if _, ok := liveProps[p.XMLName]; ok {
299 pstatForbidden.Props = append(pstatForbidden.Props, Property{XMLName: p.XMLName})
300 } else {
301 pstatFailedDep.Props = append(pstatFailedDep.Props, Property{XMLName: p.XMLName})
302 }
303 }
304 }
305 return makePropstats(pstatForbidden, pstatFailedDep), nil
306 }
307
308 f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0)
309 if err != nil {
310 return nil, err
311 }
312 defer f.Close()
313 if dph, ok := f.(DeadPropsHolder); ok {
314 ret, err := dph.Patch(patches)
315 if err != nil {
316 return nil, err
317 }
318
319
320
321 for _, pstat := range ret {
322 for i, p := range pstat.Props {
323 pstat.Props[i] = Property{XMLName: p.XMLName}
324 }
325 }
326 return ret, nil
327 }
328
329
330 pstat := Propstat{Status: http.StatusForbidden}
331 for _, patch := range patches {
332 for _, p := range patch.Props {
333 pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
334 }
335 }
336 return []Propstat{pstat}, nil
337 }
338
339 func escapeXML(s string) string {
340 for i := 0; i < len(s); i++ {
341
342
343
344 switch c := s[i]; {
345 case c == ' ' || c == '_' ||
346 ('+' <= c && c <= '9') ||
347 ('A' <= c && c <= 'Z') ||
348 ('a' <= c && c <= 'z'):
349 continue
350 }
351
352 var buf bytes.Buffer
353 xml.EscapeText(&buf, []byte(s))
354 return buf.String()
355 }
356 return s
357 }
358
359 func findResourceType(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
360 if fi.IsDir() {
361 return `<D:collection xmlns:D="DAV:"/>`, nil
362 }
363 return "", nil
364 }
365
366 func findDisplayName(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
367 if slashClean(name) == "/" {
368
369 return "", nil
370 }
371 return escapeXML(fi.Name()), nil
372 }
373
374 func findContentLength(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
375 return strconv.FormatInt(fi.Size(), 10), nil
376 }
377
378 func findLastModified(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
379 return fi.ModTime().UTC().Format(http.TimeFormat), nil
380 }
381
382
383
384 var ErrNotImplemented = errors.New("not implemented")
385
386
387
388
389
390
391
392
393
394 type ContentTyper interface {
395
396
397
398
399
400 ContentType(ctx context.Context) (string, error)
401 }
402
403 func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
404 if do, ok := fi.(ContentTyper); ok {
405 ctype, err := do.ContentType(ctx)
406 if err != ErrNotImplemented {
407 return ctype, err
408 }
409 }
410 f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
411 if err != nil {
412 return "", err
413 }
414 defer f.Close()
415
416 ctype := mime.TypeByExtension(filepath.Ext(name))
417 if ctype != "" {
418 return ctype, nil
419 }
420
421 var buf [512]byte
422 n, err := io.ReadFull(f, buf[:])
423 if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
424 return "", err
425 }
426 ctype = http.DetectContentType(buf[:n])
427
428 _, err = f.Seek(0, io.SeekStart)
429 return ctype, err
430 }
431
432
433
434
435
436
437
438
439
440 type ETager interface {
441
442
443
444
445
446
447 ETag(ctx context.Context) (string, error)
448 }
449
450 func findETag(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
451 if do, ok := fi.(ETager); ok {
452 etag, err := do.ETag(ctx)
453 if err != ErrNotImplemented {
454 return etag, err
455 }
456 }
457
458
459
460 return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil
461 }
462
463 func findSupportedLock(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
464 return `` +
465 `<D:lockentry xmlns:D="DAV:">` +
466 `<D:lockscope><D:exclusive/></D:lockscope>` +
467 `<D:locktype><D:write/></D:locktype>` +
468 `</D:lockentry>`, nil
469 }
470
View as plain text