github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/utils/etag.go (about)

     1  package utils
     2  
     3  import (
     4  	"net/http"
     5  	"net/textproto"
     6  	"strings"
     7  )
     8  
     9  // CheckPreconditions evaluates request preconditions based only on the Etag
    10  // values.
    11  func CheckPreconditions(w http.ResponseWriter, r *http.Request, etag string) (done bool) {
    12  	inm := r.Header.Get("If-None-Match")
    13  
    14  	if inm != "" && etag != "" && checkIfNoneMatch(inm, etag) {
    15  		h := w.Header()
    16  		delete(h, "Content-Type")
    17  		delete(h, "Content-Length")
    18  		w.WriteHeader(http.StatusNotModified)
    19  		return true
    20  	}
    21  
    22  	return false
    23  }
    24  
    25  func checkIfNoneMatch(ifNoneMatch, definedETag string) (match bool) {
    26  	buf := ifNoneMatch
    27  	for {
    28  		buf = textproto.TrimString(buf)
    29  		if len(buf) == 0 {
    30  			break
    31  		}
    32  		if buf[0] == ',' {
    33  			buf = buf[1:]
    34  		}
    35  		if buf[0] == '*' {
    36  			return true
    37  		}
    38  		etag, remain := scanETag(buf)
    39  		if etag == "" {
    40  			break
    41  		}
    42  		if etagWeakMatch(etag, definedETag) {
    43  			return true
    44  		}
    45  		buf = remain
    46  	}
    47  	return false
    48  }
    49  
    50  // etagWeakMatch reports whether a and b match using weak ETag comparison.
    51  // Assumes a and b are valid ETags.
    52  // More at: https://www.rfc-editor.org/rfc/rfc9110#name-comparison-2
    53  func etagWeakMatch(a, b string) bool {
    54  	return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
    55  }
    56  
    57  // scanETag determines if a syntactically valid ETag is present at s. If so,
    58  // the ETag and remaining text after consuming ETag is returned. Otherwise,
    59  // it returns "", "".
    60  func scanETag(s string) (etag string, remain string) {
    61  	start := 0
    62  	if len(s) >= 2 && s[0] == 'W' && s[1] == '/' {
    63  		start = 2
    64  	}
    65  	if len(s[start:]) < 2 || s[start] != '"' {
    66  		return "", ""
    67  	}
    68  	// ETag is either W/"text" or "text".
    69  	// See RFC 7232 2.3.
    70  	for i := start + 1; i < len(s); i++ {
    71  		c := s[i]
    72  		switch {
    73  		// Character values allowed in ETags.
    74  		case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
    75  		case c == '"':
    76  			return s[:i+1], s[i+1:]
    77  		default:
    78  			return "", ""
    79  		}
    80  	}
    81  	return "", ""
    82  }