|
|
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// HTTP file system request handler
package httputil
import ( "net/http" "net/textproto" "strings" "time" )
// scanETag determines if a syntactically valid ETag is present at s. If so,
// the ETag and remaining text after consuming ETag is returned. Otherwise,
// it returns "", "".
func scanETag(s string) (etag string, remain string) { s = textproto.TrimString(s) start := 0 if strings.HasPrefix(s, "W/") { start = 2 } if len(s[start:]) < 2 || s[start] != '"' { return "", "" } // ETag is either W/"text" or "text".
// See RFC 7232 2.3.
for i := start + 1; i < len(s); i++ { c := s[i] switch { // Character values allowed in ETags.
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: case c == '"': return s[:i+1], s[i+1:] default: return "", "" } } return "", "" }
// etagStrongMatch reports whether a and b match using strong ETag comparison.
// Assumes a and b are valid ETags.
func etagStrongMatch(a, b string) bool { return a == b && a != "" && a[0] == '"' }
// etagWeakMatch reports whether a and b match using weak ETag comparison.
// Assumes a and b are valid ETags.
func etagWeakMatch(a, b string) bool { return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") }
// condResult is the result of an HTTP request precondition check.
// See https://tools.ietf.org/html/rfc7232 section 3.
type condResult int
const ( condNone condResult = iota condTrue condFalse )
func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult { im := r.Header.Get("If-Match") if im == "" { return condNone } for { im = textproto.TrimString(im) if len(im) == 0 { break } if im[0] == ',' { im = im[1:] continue } if im[0] == '*' { return condTrue } etag, remain := scanETag(im) if etag == "" { break } if etagStrongMatch(etag, w.Header().Get("Etag")) { return condTrue } im = remain }
return condFalse }
func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult { ius := r.Header.Get("If-Unmodified-Since") if ius == "" || isZeroTime(modtime) { return condNone } if t, err := http.ParseTime(ius); err == nil { // The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if modtime.Before(t.Add(1 * time.Second)) { return condTrue } return condFalse } return condNone }
func checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult { inm := r.Header.Get("If-None-Match") if inm == "" { return condNone } buf := inm for { buf = textproto.TrimString(buf) if len(buf) == 0 { break } if buf[0] == ',' { buf = buf[1:] } if buf[0] == '*' { return condFalse } etag, remain := scanETag(buf) if etag == "" { break } if etagWeakMatch(etag, w.Header().Get("Etag")) { return condFalse } buf = remain } return condTrue }
func checkIfModifiedSince(r *http.Request, modtime time.Time) condResult { if r.Method != "GET" && r.Method != "HEAD" { return condNone } ims := r.Header.Get("If-Modified-Since") if ims == "" || isZeroTime(modtime) { return condNone } t, err := http.ParseTime(ims) if err != nil { return condNone } // The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if modtime.Before(t.Add(1 * time.Second)) { return condFalse } return condTrue }
var unixEpochTime = time.Unix(0, 0)
// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
func isZeroTime(t time.Time) bool { return t.IsZero() || t.Equal(unixEpochTime) }
func setLastModified(w http.ResponseWriter, modtime time.Time) { if !isZeroTime(modtime) { w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) } }
func writeNotModified(w http.ResponseWriter) { // RFC 7232 section 4.1:
// a sender SHOULD NOT generate representation metadata other than the
// above listed fields unless said metadata exists for the purpose of
// guiding cache updates (e.g., Last-Modified might be useful if the
// response does not have an ETag field).
h := w.Header() delete(h, "Content-Type") delete(h, "Content-Length") if h.Get("Etag") != "" { delete(h, "Last-Modified") } w.WriteHeader(http.StatusNotModified) }
// CheckPreconditions evaluates request preconditions and reports whether a precondition
// resulted in sending StatusNotModified or StatusPreconditionFailed.
func CheckPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool) { // This function carefully follows RFC 7232 section 6.
ch := checkIfMatch(w, r) if ch == condNone { ch = checkIfUnmodifiedSince(r, modtime) } if ch == condFalse { w.WriteHeader(http.StatusPreconditionFailed) return true } switch checkIfNoneMatch(w, r) { case condFalse: if r.Method == "GET" || r.Method == "HEAD" { writeNotModified(w) return true } else { w.WriteHeader(http.StatusPreconditionFailed) return true } case condNone: if checkIfModifiedSince(r, modtime) == condFalse { writeNotModified(w) return true } }
return false }
|