github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/net/webdav/prop.go (about)

     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  
     5  package webdav
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"mime"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  
    16  	"golang.org/x/net/webdav/internal/xml"
    17  )
    18  
    19  // Proppatch describes a property update instruction as defined in RFC 4918.
    20  // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
    21  type Proppatch struct {
    22  	// Remove specifies whether this patch removes properties. If it does not
    23  	// remove them, it sets them.
    24  	Remove bool
    25  	// Props contains the properties to be set or removed.
    26  	Props []Property
    27  }
    28  
    29  // Propstat describes a XML propstat element as defined in RFC 4918.
    30  // See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
    31  type Propstat struct {
    32  	// Props contains the properties for which Status applies.
    33  	Props []Property
    34  
    35  	// Status defines the HTTP status code of the properties in Prop.
    36  	// Allowed values include, but are not limited to the WebDAV status
    37  	// code extensions for HTTP/1.1.
    38  	// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
    39  	Status int
    40  
    41  	// XMLError contains the XML representation of the optional error element.
    42  	// XML content within this field must not rely on any predefined
    43  	// namespace declarations or prefixes. If empty, the XML error element
    44  	// is omitted.
    45  	XMLError string
    46  
    47  	// ResponseDescription contains the contents of the optional
    48  	// responsedescription field. If empty, the XML element is omitted.
    49  	ResponseDescription string
    50  }
    51  
    52  // makePropstats returns a slice containing those of x and y whose Props slice
    53  // is non-empty. If both are empty, it returns a slice containing an otherwise
    54  // zero Propstat whose HTTP status code is 200 OK.
    55  func makePropstats(x, y Propstat) []Propstat {
    56  	pstats := make([]Propstat, 0, 2)
    57  	if len(x.Props) != 0 {
    58  		pstats = append(pstats, x)
    59  	}
    60  	if len(y.Props) != 0 {
    61  		pstats = append(pstats, y)
    62  	}
    63  	if len(pstats) == 0 {
    64  		pstats = append(pstats, Propstat{
    65  			Status: http.StatusOK,
    66  		})
    67  	}
    68  	return pstats
    69  }
    70  
    71  // DeadPropsHolder holds the dead properties of a resource.
    72  //
    73  // Dead properties are those properties that are explicitly defined. In
    74  // comparison, live properties, such as DAV:getcontentlength, are implicitly
    75  // defined by the underlying resource, and cannot be explicitly overridden or
    76  // removed. See the Terminology section of
    77  // http://www.webdav.org/specs/rfc4918.html#rfc.section.3
    78  //
    79  // There is a whitelist of the names of live properties. This package handles
    80  // all live properties, and will only pass non-whitelisted names to the Patch
    81  // method of DeadPropsHolder implementations.
    82  type DeadPropsHolder interface {
    83  	// DeadProps returns a copy of the dead properties held.
    84  	DeadProps() (map[xml.Name]Property, error)
    85  
    86  	// Patch patches the dead properties held.
    87  	//
    88  	// Patching is atomic; either all or no patches succeed. It returns (nil,
    89  	// non-nil) if an internal server error occurred, otherwise the Propstats
    90  	// collectively contain one Property for each proposed patch Property. If
    91  	// all patches succeed, Patch returns a slice of length one and a Propstat
    92  	// element with a 200 OK HTTP status code. If none succeed, for reasons
    93  	// other than an internal server error, no Propstat has status 200 OK.
    94  	//
    95  	// For more details on when various HTTP status codes apply, see
    96  	// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status
    97  	Patch([]Proppatch) ([]Propstat, error)
    98  }
    99  
   100  // liveProps contains all supported, protected DAV: properties.
   101  var liveProps = map[xml.Name]struct {
   102  	// findFn implements the propfind function of this property. If nil,
   103  	// it indicates a hidden property.
   104  	findFn func(FileSystem, LockSystem, string, os.FileInfo) (string, error)
   105  	// dir is true if the property applies to directories.
   106  	dir bool
   107  }{
   108  	xml.Name{Space: "DAV:", Local: "resourcetype"}: {
   109  		findFn: findResourceType,
   110  		dir:    true,
   111  	},
   112  	xml.Name{Space: "DAV:", Local: "displayname"}: {
   113  		findFn: findDisplayName,
   114  		dir:    true,
   115  	},
   116  	xml.Name{Space: "DAV:", Local: "getcontentlength"}: {
   117  		findFn: findContentLength,
   118  		dir:    false,
   119  	},
   120  	xml.Name{Space: "DAV:", Local: "getlastmodified"}: {
   121  		findFn: findLastModified,
   122  		dir:    false,
   123  	},
   124  	xml.Name{Space: "DAV:", Local: "creationdate"}: {
   125  		findFn: nil,
   126  		dir:    false,
   127  	},
   128  	xml.Name{Space: "DAV:", Local: "getcontentlanguage"}: {
   129  		findFn: nil,
   130  		dir:    false,
   131  	},
   132  	xml.Name{Space: "DAV:", Local: "getcontenttype"}: {
   133  		findFn: findContentType,
   134  		dir:    false,
   135  	},
   136  	xml.Name{Space: "DAV:", Local: "getetag"}: {
   137  		findFn: findETag,
   138  		// findETag implements ETag as the concatenated hex values of a file's
   139  		// modification time and size. This is not a reliable synchronization
   140  		// mechanism for directories, so we do not advertise getetag for DAV
   141  		// collections.
   142  		dir: false,
   143  	},
   144  
   145  	// TODO: The lockdiscovery property requires LockSystem to list the
   146  	// active locks on a resource.
   147  	xml.Name{Space: "DAV:", Local: "lockdiscovery"}: {},
   148  	xml.Name{Space: "DAV:", Local: "supportedlock"}: {
   149  		findFn: findSupportedLock,
   150  		dir:    true,
   151  	},
   152  }
   153  
   154  // TODO(nigeltao) merge props and allprop?
   155  
   156  // Props returns the status of the properties named pnames for resource name.
   157  //
   158  // Each Propstat has a unique status and each property name will only be part
   159  // of one Propstat element.
   160  func props(fs FileSystem, ls LockSystem, name string, pnames []xml.Name) ([]Propstat, error) {
   161  	f, err := fs.OpenFile(name, os.O_RDONLY, 0)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	defer f.Close()
   166  	fi, err := f.Stat()
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	isDir := fi.IsDir()
   171  
   172  	var deadProps map[xml.Name]Property
   173  	if dph, ok := f.(DeadPropsHolder); ok {
   174  		deadProps, err = dph.DeadProps()
   175  		if err != nil {
   176  			return nil, err
   177  		}
   178  	}
   179  
   180  	pstatOK := Propstat{Status: http.StatusOK}
   181  	pstatNotFound := Propstat{Status: http.StatusNotFound}
   182  	for _, pn := range pnames {
   183  		// If this file has dead properties, check if they contain pn.
   184  		if dp, ok := deadProps[pn]; ok {
   185  			pstatOK.Props = append(pstatOK.Props, dp)
   186  			continue
   187  		}
   188  		// Otherwise, it must either be a live property or we don't know it.
   189  		if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
   190  			innerXML, err := prop.findFn(fs, ls, name, fi)
   191  			if err != nil {
   192  				return nil, err
   193  			}
   194  			pstatOK.Props = append(pstatOK.Props, Property{
   195  				XMLName:  pn,
   196  				InnerXML: []byte(innerXML),
   197  			})
   198  		} else {
   199  			pstatNotFound.Props = append(pstatNotFound.Props, Property{
   200  				XMLName: pn,
   201  			})
   202  		}
   203  	}
   204  	return makePropstats(pstatOK, pstatNotFound), nil
   205  }
   206  
   207  // Propnames returns the property names defined for resource name.
   208  func propnames(fs FileSystem, ls LockSystem, name string) ([]xml.Name, error) {
   209  	f, err := fs.OpenFile(name, os.O_RDONLY, 0)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	defer f.Close()
   214  	fi, err := f.Stat()
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	isDir := fi.IsDir()
   219  
   220  	var deadProps map[xml.Name]Property
   221  	if dph, ok := f.(DeadPropsHolder); ok {
   222  		deadProps, err = dph.DeadProps()
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  	}
   227  
   228  	pnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
   229  	for pn, prop := range liveProps {
   230  		if prop.findFn != nil && (prop.dir || !isDir) {
   231  			pnames = append(pnames, pn)
   232  		}
   233  	}
   234  	for pn := range deadProps {
   235  		pnames = append(pnames, pn)
   236  	}
   237  	return pnames, nil
   238  }
   239  
   240  // Allprop returns the properties defined for resource name and the properties
   241  // named in include.
   242  //
   243  // Note that RFC 4918 defines 'allprop' to return the DAV: properties defined
   244  // within the RFC plus dead properties. Other live properties should only be
   245  // returned if they are named in 'include'.
   246  //
   247  // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
   248  func allprop(fs FileSystem, ls LockSystem, name string, include []xml.Name) ([]Propstat, error) {
   249  	pnames, err := propnames(fs, ls, name)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	// Add names from include if they are not already covered in pnames.
   254  	nameset := make(map[xml.Name]bool)
   255  	for _, pn := range pnames {
   256  		nameset[pn] = true
   257  	}
   258  	for _, pn := range include {
   259  		if !nameset[pn] {
   260  			pnames = append(pnames, pn)
   261  		}
   262  	}
   263  	return props(fs, ls, name, pnames)
   264  }
   265  
   266  // Patch patches the properties of resource name. The return values are
   267  // constrained in the same manner as DeadPropsHolder.Patch.
   268  func patch(fs FileSystem, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
   269  	conflict := false
   270  loop:
   271  	for _, patch := range patches {
   272  		for _, p := range patch.Props {
   273  			if _, ok := liveProps[p.XMLName]; ok {
   274  				conflict = true
   275  				break loop
   276  			}
   277  		}
   278  	}
   279  	if conflict {
   280  		pstatForbidden := Propstat{
   281  			Status:   http.StatusForbidden,
   282  			XMLError: `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`,
   283  		}
   284  		pstatFailedDep := Propstat{
   285  			Status: StatusFailedDependency,
   286  		}
   287  		for _, patch := range patches {
   288  			for _, p := range patch.Props {
   289  				if _, ok := liveProps[p.XMLName]; ok {
   290  					pstatForbidden.Props = append(pstatForbidden.Props, Property{XMLName: p.XMLName})
   291  				} else {
   292  					pstatFailedDep.Props = append(pstatFailedDep.Props, Property{XMLName: p.XMLName})
   293  				}
   294  			}
   295  		}
   296  		return makePropstats(pstatForbidden, pstatFailedDep), nil
   297  	}
   298  
   299  	f, err := fs.OpenFile(name, os.O_RDWR, 0)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  	defer f.Close()
   304  	if dph, ok := f.(DeadPropsHolder); ok {
   305  		ret, err := dph.Patch(patches)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  		// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat says that
   310  		// "The contents of the prop XML element must only list the names of
   311  		// properties to which the result in the status element applies."
   312  		for _, pstat := range ret {
   313  			for i, p := range pstat.Props {
   314  				pstat.Props[i] = Property{XMLName: p.XMLName}
   315  			}
   316  		}
   317  		return ret, nil
   318  	}
   319  	// The file doesn't implement the optional DeadPropsHolder interface, so
   320  	// all patches are forbidden.
   321  	pstat := Propstat{Status: http.StatusForbidden}
   322  	for _, patch := range patches {
   323  		for _, p := range patch.Props {
   324  			pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
   325  		}
   326  	}
   327  	return []Propstat{pstat}, nil
   328  }
   329  
   330  func findResourceType(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   331  	if fi.IsDir() {
   332  		return `<D:collection xmlns:D="DAV:"/>`, nil
   333  	}
   334  	return "", nil
   335  }
   336  
   337  func findDisplayName(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   338  	if slashClean(name) == "/" {
   339  		// Hide the real name of a possibly prefixed root directory.
   340  		return "", nil
   341  	}
   342  	return fi.Name(), nil
   343  }
   344  
   345  func findContentLength(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   346  	return strconv.FormatInt(fi.Size(), 10), nil
   347  }
   348  
   349  func findLastModified(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   350  	return fi.ModTime().Format(http.TimeFormat), nil
   351  }
   352  
   353  func findContentType(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   354  	f, err := fs.OpenFile(name, os.O_RDONLY, 0)
   355  	if err != nil {
   356  		return "", err
   357  	}
   358  	defer f.Close()
   359  	// This implementation is based on serveContent's code in the standard net/http package.
   360  	ctype := mime.TypeByExtension(filepath.Ext(name))
   361  	if ctype != "" {
   362  		return ctype, nil
   363  	}
   364  	// Read a chunk to decide between utf-8 text and binary.
   365  	var buf [512]byte
   366  	n, err := io.ReadFull(f, buf[:])
   367  	if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
   368  		return "", err
   369  	}
   370  	ctype = http.DetectContentType(buf[:n])
   371  	// Rewind file.
   372  	_, err = f.Seek(0, os.SEEK_SET)
   373  	return ctype, err
   374  }
   375  
   376  func findETag(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   377  	// The Apache http 2.4 web server by default concatenates the
   378  	// modification time and size of a file. We replicate the heuristic
   379  	// with nanosecond granularity.
   380  	return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil
   381  }
   382  
   383  func findSupportedLock(fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) {
   384  	return `` +
   385  		`<D:lockentry xmlns:D="DAV:">` +
   386  		`<D:lockscope><D:exclusive/></D:lockscope>` +
   387  		`<D:locktype><D:write/></D:locktype>` +
   388  		`</D:lockentry>`, nil
   389  }