golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/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  	"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  // Proppatch describes a property update instruction as defined in RFC 4918.
    22  // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
    23  type Proppatch struct {
    24  	// Remove specifies whether this patch removes properties. If it does not
    25  	// remove them, it sets them.
    26  	Remove bool
    27  	// Props contains the properties to be set or removed.
    28  	Props []Property
    29  }
    30  
    31  // Propstat describes a XML propstat element as defined in RFC 4918.
    32  // See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
    33  type Propstat struct {
    34  	// Props contains the properties for which Status applies.
    35  	Props []Property
    36  
    37  	// Status defines the HTTP status code of the properties in Prop.
    38  	// Allowed values include, but are not limited to the WebDAV status
    39  	// code extensions for HTTP/1.1.
    40  	// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
    41  	Status int
    42  
    43  	// XMLError contains the XML representation of the optional error element.
    44  	// XML content within this field must not rely on any predefined
    45  	// namespace declarations or prefixes. If empty, the XML error element
    46  	// is omitted.
    47  	XMLError string
    48  
    49  	// ResponseDescription contains the contents of the optional
    50  	// responsedescription field. If empty, the XML element is omitted.
    51  	ResponseDescription string
    52  }
    53  
    54  // makePropstats returns a slice containing those of x and y whose Props slice
    55  // is non-empty. If both are empty, it returns a slice containing an otherwise
    56  // zero Propstat whose HTTP status code is 200 OK.
    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  // DeadPropsHolder holds the dead properties of a resource.
    74  //
    75  // Dead properties are those properties that are explicitly defined. In
    76  // comparison, live properties, such as DAV:getcontentlength, are implicitly
    77  // defined by the underlying resource, and cannot be explicitly overridden or
    78  // removed. See the Terminology section of
    79  // http://www.webdav.org/specs/rfc4918.html#rfc.section.3
    80  //
    81  // There is a whitelist of the names of live properties. This package handles
    82  // all live properties, and will only pass non-whitelisted names to the Patch
    83  // method of DeadPropsHolder implementations.
    84  type DeadPropsHolder interface {
    85  	// DeadProps returns a copy of the dead properties held.
    86  	DeadProps() (map[xml.Name]Property, error)
    87  
    88  	// Patch patches the dead properties held.
    89  	//
    90  	// Patching is atomic; either all or no patches succeed. It returns (nil,
    91  	// non-nil) if an internal server error occurred, otherwise the Propstats
    92  	// collectively contain one Property for each proposed patch Property. If
    93  	// all patches succeed, Patch returns a slice of length one and a Propstat
    94  	// element with a 200 OK HTTP status code. If none succeed, for reasons
    95  	// other than an internal server error, no Propstat has status 200 OK.
    96  	//
    97  	// For more details on when various HTTP status codes apply, see
    98  	// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status
    99  	Patch([]Proppatch) ([]Propstat, error)
   100  }
   101  
   102  // liveProps contains all supported, protected DAV: properties.
   103  var liveProps = map[xml.Name]struct {
   104  	// findFn implements the propfind function of this property. If nil,
   105  	// it indicates a hidden property.
   106  	findFn func(context.Context, FileSystem, LockSystem, string, os.FileInfo) (string, error)
   107  	// dir is true if the property applies to directories.
   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  		// http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified
   125  		// suggests that getlastmodified should only apply to GETable
   126  		// resources, and this package does not support GET on directories.
   127  		//
   128  		// Nonetheless, some WebDAV clients expect child directories to be
   129  		// sortable by getlastmodified date, so this value is true, not false.
   130  		// See golang.org/issue/15334.
   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  		// findETag implements ETag as the concatenated hex values of a file's
   148  		// modification time and size. This is not a reliable synchronization
   149  		// mechanism for directories, so we do not advertise getetag for DAV
   150  		// collections.
   151  		dir: false,
   152  	},
   153  
   154  	// TODO: The lockdiscovery property requires LockSystem to list the
   155  	// active locks on a resource.
   156  	{Space: "DAV:", Local: "lockdiscovery"}: {},
   157  	{Space: "DAV:", Local: "supportedlock"}: {
   158  		findFn: findSupportedLock,
   159  		dir:    true,
   160  	},
   161  }
   162  
   163  // TODO(nigeltao) merge props and allprop?
   164  
   165  // props returns the status of the properties named pnames for resource name.
   166  //
   167  // Each Propstat has a unique status and each property name will only be part
   168  // of one Propstat element.
   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  		// If this file has dead properties, check if they contain pn.
   193  		if dp, ok := deadProps[pn]; ok {
   194  			pstatOK.Props = append(pstatOK.Props, dp)
   195  			continue
   196  		}
   197  		// Otherwise, it must either be a live property or we don't know it.
   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  // propnames returns the property names defined for resource name.
   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  // allprop returns the properties defined for resource name and the properties
   250  // named in include.
   251  //
   252  // Note that RFC 4918 defines 'allprop' to return the DAV: properties defined
   253  // within the RFC plus dead properties. Other live properties should only be
   254  // returned if they are named in 'include'.
   255  //
   256  // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
   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  	// Add names from include if they are not already covered in pnames.
   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  // patch patches the properties of resource name. The return values are
   276  // constrained in the same manner as DeadPropsHolder.Patch.
   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  		// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat says that
   319  		// "The contents of the prop XML element must only list the names of
   320  		// properties to which the result in the status element applies."
   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  	// The file doesn't implement the optional DeadPropsHolder interface, so
   329  	// all patches are forbidden.
   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  		// As an optimization, if s contains only ASCII letters, digits or a
   342  		// few special characters, the escaped value is s itself and we don't
   343  		// need to allocate a buffer and convert between string and []byte.
   344  		switch c := s[i]; {
   345  		case c == ' ' || c == '_' ||
   346  			('+' <= c && c <= '9') || // Digits as well as + , - . and /
   347  			('A' <= c && c <= 'Z') ||
   348  			('a' <= c && c <= 'z'):
   349  			continue
   350  		}
   351  		// Otherwise, go through the full escaping process.
   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  		// Hide the real name of a possibly prefixed root directory.
   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  // ErrNotImplemented should be returned by optional interfaces if they
   383  // want the original implementation to be used.
   384  var ErrNotImplemented = errors.New("not implemented")
   385  
   386  // ContentTyper is an optional interface for the os.FileInfo
   387  // objects returned by the FileSystem.
   388  //
   389  // If this interface is defined then it will be used to read the
   390  // content type from the object.
   391  //
   392  // If this interface is not defined the file will be opened and the
   393  // content type will be guessed from the initial contents of the file.
   394  type ContentTyper interface {
   395  	// ContentType returns the content type for the file.
   396  	//
   397  	// If this returns error ErrNotImplemented then the error will
   398  	// be ignored and the base implementation will be used
   399  	// instead.
   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  	// This implementation is based on serveContent's code in the standard net/http package.
   416  	ctype := mime.TypeByExtension(filepath.Ext(name))
   417  	if ctype != "" {
   418  		return ctype, nil
   419  	}
   420  	// Read a chunk to decide between utf-8 text and binary.
   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  	// Rewind file.
   428  	_, err = f.Seek(0, io.SeekStart)
   429  	return ctype, err
   430  }
   431  
   432  // ETager is an optional interface for the os.FileInfo objects
   433  // returned by the FileSystem.
   434  //
   435  // If this interface is defined then it will be used to read the ETag
   436  // for the object.
   437  //
   438  // If this interface is not defined an ETag will be computed using the
   439  // ModTime() and the Size() methods of the os.FileInfo object.
   440  type ETager interface {
   441  	// ETag returns an ETag for the file.  This should be of the
   442  	// form "value" or W/"value"
   443  	//
   444  	// If this returns error ErrNotImplemented then the error will
   445  	// be ignored and the base implementation will be used
   446  	// instead.
   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  	// The Apache http 2.4 web server by default concatenates the
   458  	// modification time and size of a file. We replicate the heuristic
   459  	// with nanosecond granularity.
   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  }