1
0
Fork 0
eighty/serve/fasthttp.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)
}