prop.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // Copyright 2015 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. package webdav
  5. import (
  6. "encoding/xml"
  7. "fmt"
  8. "io"
  9. "mime"
  10. "net/http"
  11. "os"
  12. "path/filepath"
  13. "strconv"
  14. "sync"
  15. )
  16. // PropSystem manages the properties of named resources. It allows finding
  17. // and setting properties as defined in RFC 4918.
  18. //
  19. // The elements in a resource name are separated by slash ('/', U+002F)
  20. // characters, regardless of host operating system convention.
  21. type PropSystem interface {
  22. // Find returns the status of properties named propnames for resource name.
  23. //
  24. // Each Propstat must have a unique status and each property name must
  25. // only be part of one Propstat element.
  26. Find(name string, propnames []xml.Name) ([]Propstat, error)
  27. // TODO(nigeltao) merge Find and Allprop?
  28. // Allprop returns the properties defined for resource name and the
  29. // properties named in include. The returned Propstats are handled
  30. // as in Find.
  31. //
  32. // Note that RFC 4918 defines 'allprop' to return the DAV: properties
  33. // defined within the RFC plus dead properties. Other live properties
  34. // should only be returned if they are named in 'include'.
  35. //
  36. // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
  37. Allprop(name string, include []xml.Name) ([]Propstat, error)
  38. // Propnames returns the property names defined for resource name.
  39. Propnames(name string) ([]xml.Name, error)
  40. // Patch patches the properties of resource name.
  41. //
  42. // If all patches can be applied without conflict, Patch returns a slice
  43. // of length one and a Propstat element of status 200, naming all patched
  44. // properties. In case of conflict, Patch returns an arbitrary long slice
  45. // and no Propstat element must have status 200. In either case, properties
  46. // in Propstat must not have values.
  47. //
  48. // Note that the WebDAV RFC requires either all patches to succeed or none.
  49. Patch(name string, patches []Proppatch) ([]Propstat, error)
  50. // TODO(rost) COPY/MOVE/DELETE.
  51. }
  52. // Proppatch describes a property update instruction as defined in RFC 4918.
  53. // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
  54. type Proppatch struct {
  55. // Remove specifies whether this patch removes properties. If it does not
  56. // remove them, it sets them.
  57. Remove bool
  58. // Props contains the properties to be set or removed.
  59. Props []Property
  60. }
  61. // Propstat describes a XML propstat element as defined in RFC 4918.
  62. // See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
  63. type Propstat struct {
  64. // Props contains the properties for which Status applies.
  65. Props []Property
  66. // Status defines the HTTP status code of the properties in Prop.
  67. // Allowed values include, but are not limited to the WebDAV status
  68. // code extensions for HTTP/1.1.
  69. // http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
  70. Status int
  71. // XMLError contains the XML representation of the optional error element.
  72. // XML content within this field must not rely on any predefined
  73. // namespace declarations or prefixes. If empty, the XML error element
  74. // is omitted.
  75. XMLError string
  76. // ResponseDescription contains the contents of the optional
  77. // responsedescription field. If empty, the XML element is omitted.
  78. ResponseDescription string
  79. }
  80. // memPS implements an in-memory PropSystem. It supports all of the mandatory
  81. // live properties of RFC 4918.
  82. type memPS struct {
  83. fs FileSystem
  84. ls LockSystem
  85. m Mutability
  86. mu sync.RWMutex
  87. nodes map[string]*memPSNode
  88. }
  89. // memPSNode stores the dead properties of a resource.
  90. type memPSNode struct {
  91. mu sync.RWMutex
  92. deadProps map[xml.Name]Property
  93. }
  94. // BUG(rost): In this development version, the in-memory property system does
  95. // not handle COPY/MOVE/DELETE requests. As a result, dead properties are not
  96. // released if the according DAV resource is deleted or moved. It is not
  97. // recommended to use a read-writeable property system in production.
  98. // Mutability indicates the mutability of a property system.
  99. type Mutability bool
  100. const (
  101. ReadOnly = Mutability(false)
  102. ReadWrite = Mutability(true)
  103. )
  104. // NewMemPS returns a new in-memory PropSystem implementation. A read-only
  105. // property system rejects all patches. A read-writeable property system
  106. // stores arbitrary properties but refuses to change any DAV: property
  107. // specified in RFC 4918. It imposes no limit on the size of property values.
  108. func NewMemPS(fs FileSystem, ls LockSystem, m Mutability) PropSystem {
  109. return &memPS{
  110. fs: fs,
  111. ls: ls,
  112. m: m,
  113. nodes: make(map[string]*memPSNode),
  114. }
  115. }
  116. // liveProps contains all supported, protected DAV: properties.
  117. var liveProps = map[xml.Name]struct {
  118. // findFn implements the propfind function of this property. If nil,
  119. // it indicates a hidden property.
  120. findFn func(*memPS, string, os.FileInfo) (string, error)
  121. // dir is true if the property applies to directories.
  122. dir bool
  123. }{
  124. xml.Name{Space: "DAV:", Local: "resourcetype"}: {
  125. findFn: (*memPS).findResourceType,
  126. dir: true,
  127. },
  128. xml.Name{Space: "DAV:", Local: "displayname"}: {
  129. findFn: (*memPS).findDisplayName,
  130. dir: true,
  131. },
  132. xml.Name{Space: "DAV:", Local: "getcontentlength"}: {
  133. findFn: (*memPS).findContentLength,
  134. dir: true,
  135. },
  136. xml.Name{Space: "DAV:", Local: "getlastmodified"}: {
  137. findFn: (*memPS).findLastModified,
  138. dir: true,
  139. },
  140. xml.Name{Space: "DAV:", Local: "creationdate"}: {
  141. findFn: nil,
  142. dir: true,
  143. },
  144. xml.Name{Space: "DAV:", Local: "getcontentlanguage"}: {
  145. findFn: nil,
  146. dir: true,
  147. },
  148. xml.Name{Space: "DAV:", Local: "getcontenttype"}: {
  149. findFn: (*memPS).findContentType,
  150. dir: true,
  151. },
  152. xml.Name{Space: "DAV:", Local: "getetag"}: {
  153. findFn: (*memPS).findETag,
  154. // memPS implements ETag as the concatenated hex values of a file's
  155. // modification time and size. This is not a reliable synchronization
  156. // mechanism for directories, so we do not advertise getetag for
  157. // DAV collections.
  158. dir: false,
  159. },
  160. // TODO(nigeltao) Lock properties will be defined later.
  161. xml.Name{Space: "DAV:", Local: "lockdiscovery"}: {},
  162. xml.Name{Space: "DAV:", Local: "supportedlock"}: {},
  163. }
  164. func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) {
  165. ps.mu.RLock()
  166. defer ps.mu.RUnlock()
  167. fi, err := ps.fs.Stat(name)
  168. if err != nil {
  169. return nil, err
  170. }
  171. // Lookup the dead properties of this resource. It's OK if there are none.
  172. n, ok := ps.nodes[name]
  173. if ok {
  174. n.mu.RLock()
  175. defer n.mu.RUnlock()
  176. }
  177. pm := make(map[int]Propstat)
  178. for _, pn := range propnames {
  179. // If this node has dead properties, check if they contain pn.
  180. if n != nil {
  181. if dp, ok := n.deadProps[pn]; ok {
  182. pstat := pm[http.StatusOK]
  183. pstat.Props = append(pstat.Props, dp)
  184. pm[http.StatusOK] = pstat
  185. continue
  186. }
  187. }
  188. // Otherwise, it must either be a live property or we don't know it.
  189. p := Property{XMLName: pn}
  190. s := http.StatusNotFound
  191. if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !fi.IsDir()) {
  192. xmlvalue, err := prop.findFn(ps, name, fi)
  193. if err != nil {
  194. return nil, err
  195. }
  196. s = http.StatusOK
  197. p.InnerXML = []byte(xmlvalue)
  198. }
  199. pstat := pm[s]
  200. pstat.Props = append(pstat.Props, p)
  201. pm[s] = pstat
  202. }
  203. pstats := make([]Propstat, 0, len(pm))
  204. for s, pstat := range pm {
  205. pstat.Status = s
  206. pstats = append(pstats, pstat)
  207. }
  208. return pstats, nil
  209. }
  210. func (ps *memPS) Propnames(name string) ([]xml.Name, error) {
  211. fi, err := ps.fs.Stat(name)
  212. if err != nil {
  213. return nil, err
  214. }
  215. propnames := make([]xml.Name, 0, len(liveProps))
  216. for pn, prop := range liveProps {
  217. if prop.findFn != nil && (prop.dir || !fi.IsDir()) {
  218. propnames = append(propnames, pn)
  219. }
  220. }
  221. ps.mu.RLock()
  222. defer ps.mu.RUnlock()
  223. if n, ok := ps.nodes[name]; ok {
  224. n.mu.RLock()
  225. defer n.mu.RUnlock()
  226. for pn := range n.deadProps {
  227. propnames = append(propnames, pn)
  228. }
  229. }
  230. return propnames, nil
  231. }
  232. func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) {
  233. propnames, err := ps.Propnames(name)
  234. if err != nil {
  235. return nil, err
  236. }
  237. // Add names from include if they are not already covered in propnames.
  238. nameset := make(map[xml.Name]bool)
  239. for _, pn := range propnames {
  240. nameset[pn] = true
  241. }
  242. for _, pn := range include {
  243. if !nameset[pn] {
  244. propnames = append(propnames, pn)
  245. }
  246. }
  247. return ps.Find(name, propnames)
  248. }
  249. func (ps *memPS) Patch(name string, patches []Proppatch) ([]Propstat, error) {
  250. // A DELETE/COPY/MOVE might fly in, so we need to keep all nodes locked until
  251. // the end of this PROPPATCH.
  252. ps.mu.Lock()
  253. defer ps.mu.Unlock()
  254. n, ok := ps.nodes[name]
  255. if !ok {
  256. n = &memPSNode{deadProps: make(map[xml.Name]Property)}
  257. }
  258. n.mu.Lock()
  259. defer n.mu.Unlock()
  260. _, err := ps.fs.Stat(name)
  261. if err != nil {
  262. return nil, err
  263. }
  264. // Perform a dry-run to identify any patch conflicts. A read-only property
  265. // system always fails at this stage.
  266. pm := make(map[int]Propstat)
  267. for _, patch := range patches {
  268. for _, p := range patch.Props {
  269. s := http.StatusOK
  270. if _, ok := liveProps[p.XMLName]; ok || ps.m == ReadOnly {
  271. s = http.StatusForbidden
  272. }
  273. pstat := pm[s]
  274. pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
  275. pm[s] = pstat
  276. }
  277. }
  278. // Based on the dry-run, either apply the patches or handle conflicts.
  279. if _, ok = pm[http.StatusOK]; ok {
  280. if len(pm) == 1 {
  281. for _, patch := range patches {
  282. for _, p := range patch.Props {
  283. if patch.Remove {
  284. delete(n.deadProps, p.XMLName)
  285. } else {
  286. n.deadProps[p.XMLName] = p
  287. }
  288. }
  289. }
  290. ps.nodes[name] = n
  291. } else {
  292. pm[StatusFailedDependency] = pm[http.StatusOK]
  293. delete(pm, http.StatusOK)
  294. }
  295. }
  296. pstats := make([]Propstat, 0, len(pm))
  297. for s, pstat := range pm {
  298. pstat.Status = s
  299. pstats = append(pstats, pstat)
  300. }
  301. return pstats, nil
  302. }
  303. func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) {
  304. if fi.IsDir() {
  305. return `<collection xmlns="DAV:"/>`, nil
  306. }
  307. return "", nil
  308. }
  309. func (ps *memPS) findDisplayName(name string, fi os.FileInfo) (string, error) {
  310. if slashClean(name) == "/" {
  311. // Hide the real name of a possibly prefixed root directory.
  312. return "", nil
  313. }
  314. return fi.Name(), nil
  315. }
  316. func (ps *memPS) findContentLength(name string, fi os.FileInfo) (string, error) {
  317. return strconv.FormatInt(fi.Size(), 10), nil
  318. }
  319. func (ps *memPS) findLastModified(name string, fi os.FileInfo) (string, error) {
  320. return fi.ModTime().Format(http.TimeFormat), nil
  321. }
  322. func (ps *memPS) findContentType(name string, fi os.FileInfo) (string, error) {
  323. f, err := ps.fs.OpenFile(name, os.O_RDONLY, 0)
  324. if err != nil {
  325. return "", err
  326. }
  327. defer f.Close()
  328. // This implementation is based on serveContent's code in the standard net/http package.
  329. ctype := mime.TypeByExtension(filepath.Ext(name))
  330. if ctype == "" {
  331. // Read a chunk to decide between utf-8 text and binary.
  332. var buf [512]byte
  333. n, _ := io.ReadFull(f, buf[:])
  334. ctype = http.DetectContentType(buf[:n])
  335. // Rewind file.
  336. _, err = f.Seek(0, os.SEEK_SET)
  337. }
  338. return ctype, err
  339. }
  340. func (ps *memPS) findETag(name string, fi os.FileInfo) (string, error) {
  341. return detectETag(fi), nil
  342. }
  343. // detectETag determines the ETag for the file described by fi.
  344. func detectETag(fi os.FileInfo) string {
  345. // The Apache http 2.4 web server by default concatenates the
  346. // modification time and size of a file. We replicate the heuristic
  347. // with nanosecond granularity.
  348. return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
  349. }