211 lines
4.9 KiB
Go
211 lines
4.9 KiB
Go
package serve
|
|
|
|
import (
|
|
"amuz.es/src/go/eighty"
|
|
"amuz.es/src/go/misc/strutil"
|
|
"bytes"
|
|
"crypto/subtle"
|
|
"github.com/valyala/fasthttp"
|
|
"net/http"
|
|
"net/textproto"
|
|
"time"
|
|
)
|
|
|
|
const sniffLen = 512
|
|
|
|
var (
|
|
etagPrefix = []byte("W/")
|
|
|
|
unixEpochTime = time.Unix(0, 0)
|
|
)
|
|
|
|
type condResult int
|
|
|
|
const (
|
|
condNone condResult = iota
|
|
condTrue
|
|
condFalse
|
|
)
|
|
|
|
// checkPreconditions evaluates request preconditions and reports whether a precondition
|
|
// resulted in sending StatusNotModified or StatusPreconditionFailed.
|
|
func checkPreconditions(r *fasthttp.Request, w *fasthttp.Response, modtime time.Time, etag []byte) (done bool) {
|
|
// This function carefully follows RFC 7232 section 6.
|
|
ch := checkIfMatch(r, w)
|
|
if ch == condNone {
|
|
ch = checkIfUnmodifiedSince(r, modtime)
|
|
}
|
|
if ch == condFalse {
|
|
w.SetStatusCode(http.StatusPreconditionFailed)
|
|
return true
|
|
}
|
|
switch checkIfNoneMatch(etag, r, w) {
|
|
case condFalse:
|
|
if subtle.ConstantTimeCompare(r.Header.Method(), eighty.MethodGET) == 1 || subtle.ConstantTimeCompare(r.Header.Method(), eighty.MethodHEAD) == 1 {
|
|
writeNotModified(w)
|
|
return true
|
|
} else {
|
|
w.SetStatusCode(http.StatusPreconditionFailed)
|
|
return true
|
|
}
|
|
case condNone:
|
|
if checkIfModifiedSince(r, modtime) == condFalse {
|
|
writeNotModified(w)
|
|
return true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkIfNoneMatch(providedEtag []byte, r *fasthttp.Request, w *fasthttp.Response) condResult {
|
|
inm := r.Header.Peek("If-None-Match")
|
|
if len(inm) == 0 {
|
|
return condNone
|
|
}
|
|
buf := inm
|
|
for {
|
|
buf = textproto.TrimBytes(buf)
|
|
if len(buf) == 0 {
|
|
break
|
|
}
|
|
if buf[0] == ',' {
|
|
buf = buf[1:]
|
|
}
|
|
if buf[0] == '*' {
|
|
return condFalse
|
|
}
|
|
etag, remain := scanETag(buf)
|
|
if len(etag) == 0 {
|
|
break
|
|
}
|
|
if etagWeakMatch(etag, providedEtag) {
|
|
return condFalse
|
|
}
|
|
buf = remain
|
|
}
|
|
return condTrue
|
|
}
|
|
|
|
func checkIfModifiedSince(r *fasthttp.Request, modtime time.Time) condResult {
|
|
if subtle.ConstantTimeCompare(r.Header.Method(), eighty.MethodGET) != 1 && subtle.ConstantTimeCompare(r.Header.Method(), eighty.MethodHEAD) != 1 {
|
|
return condNone
|
|
}
|
|
ims := r.Header.Peek("If-Modified-Since")
|
|
if len(ims) == 0 || isZeroTime(modtime) {
|
|
return condNone
|
|
}
|
|
t, err := http.ParseTime(strutil.B2S(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
|
|
}
|
|
|
|
func checkIfUnmodifiedSince(r *fasthttp.Request, modtime time.Time) condResult {
|
|
ius := r.Header.Peek("If-Unmodified-Since")
|
|
if len(ius) == 0 || isZeroTime(modtime) {
|
|
return condNone
|
|
}
|
|
if t, err := http.ParseTime(strutil.B2S(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 checkIfMatch(r *fasthttp.Request, w *fasthttp.Response) condResult {
|
|
im := r.Header.Peek("If-Match")
|
|
if len(im) == 0 {
|
|
return condNone
|
|
}
|
|
for {
|
|
im = textproto.TrimBytes(im)
|
|
if len(im) == 0 {
|
|
break
|
|
}
|
|
if im[0] == ',' {
|
|
im = im[1:]
|
|
continue
|
|
}
|
|
if im[0] == '*' {
|
|
return condTrue
|
|
}
|
|
etag, remain := scanETag(im)
|
|
if len(etag) == 0 {
|
|
break
|
|
}
|
|
if etagStrongMatch(etag, w.Header.Peek("Etag")) {
|
|
return condTrue
|
|
}
|
|
im = remain
|
|
}
|
|
|
|
return condFalse
|
|
}
|
|
|
|
func scanETag(s []byte) (etag []byte, remain []byte) {
|
|
s = textproto.TrimBytes(s)
|
|
start := 0
|
|
if bytes.HasPrefix(s, etagPrefix) {
|
|
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
|
|
}
|
|
|
|
func etagStrongMatch(a, b []byte) bool {
|
|
return subtle.ConstantTimeCompare(a, b) == 1 && len(a) > 0 && a[0] == '"'
|
|
}
|
|
|
|
// etagWeakMatch reports whether a and b match using weak ETag comparison.
|
|
// Assumes a and b are valid ETags.
|
|
func etagWeakMatch(a, b []byte) bool {
|
|
return subtle.ConstantTimeCompare(
|
|
bytes.TrimPrefix(a, etagPrefix),
|
|
bytes.TrimPrefix(b, etagPrefix),
|
|
) == 1
|
|
}
|
|
|
|
func writeNotModified(w *fasthttp.Response) {
|
|
// 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).
|
|
w.Header.Del("Content-Type")
|
|
w.Header.Del("Content-Length")
|
|
if len(w.Header.Peek("Etag")) > 0 {
|
|
w.Header.Del("Last-Modified")
|
|
}
|
|
w.SetStatusCode(http.StatusNotModified)
|
|
w.SkipBody = true
|
|
}
|
|
|
|
func isZeroTime(t time.Time) bool {
|
|
return t.IsZero() || t.Equal(unixEpochTime)
|
|
}
|