You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
5.2 KiB
218 lines
5.2 KiB
// 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
|
|
}
|