-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// 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. | ||
// | ||
// Source: Originally from Go's net/http/fs.go | ||
// https://cs.opensource.google/go/go/+/master:src/net/http/fs.go | ||
|
||
package storage | ||
|
||
import ( | ||
"net/http" | ||
"net/textproto" | ||
"strings" | ||
"time" | ||
) | ||
|
||
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(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool) { | ||
// This function carefully follows RFC 7232 section 6. | ||
ch := checkIfMatch(r) | ||
if ch == condNone { | ||
ch = checkIfUnmodifiedSince(r, modtime) | ||
} | ||
if ch == condFalse { | ||
w.WriteHeader(http.StatusPreconditionFailed) | ||
return true | ||
} | ||
switch checkIfNoneMatch(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, w, modtime) == condFalse { | ||
writeNotModified(w) | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func checkIfModifiedSince(r *http.Request, w http.ResponseWriter, modtime time.Time) condResult { | ||
ims := r.Header.Get("If-Modified-Since") | ||
if ims == "" || isZeroTime(modtime) { | ||
return condTrue | ||
} | ||
t, err := ParseTime(ims) | ||
if err != nil { | ||
httpError(w, err) | ||
return condNone | ||
} | ||
// The Last-Modified header truncates sub-second precision so | ||
// the modtime needs to be truncated too. | ||
modtime = modtime.Truncate(time.Second) | ||
if modtime.Compare(t) <= 0 { | ||
return condFalse | ||
} | ||
return condTrue | ||
} | ||
|
||
func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult { | ||
ius := r.Header.Get("If-Unmodified-Since") | ||
if ius == "" || isZeroTime(modtime) { | ||
return condNone | ||
} | ||
t, err := ParseTime(ius) | ||
if err != nil { | ||
return condNone | ||
} | ||
|
||
// The Last-Modified header truncates sub-second precision so | ||
// the modtime needs to be truncated too. | ||
modtime = modtime.Truncate(time.Second) | ||
if ret := modtime.Compare(t); ret <= 0 { | ||
return condTrue | ||
} | ||
return condFalse | ||
} | ||
|
||
// TimeFormat is the time format to use when generating times in HTTP | ||
// headers. It is like [time.RFC1123] but hard-codes GMT as the time | ||
// zone. The time being formatted must be in UTC for Format to | ||
// generate the correct format. | ||
// | ||
// For parsing this time format, see [ParseTime]. | ||
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" | ||
|
||
var ( | ||
unixEpochTime = time.Unix(0, 0) | ||
timeFormats = []string{ | ||
TimeFormat, | ||
time.RFC850, | ||
time.ANSIC, | ||
} | ||
) | ||
|
||
// 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 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") | ||
delete(h, "Content-Encoding") | ||
if h.Get("Etag") != "" { | ||
delete(h, "Last-Modified") | ||
} | ||
w.WriteHeader(http.StatusNotModified) | ||
} | ||
|
||
func checkIfNoneMatch(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:] | ||
continue | ||
} | ||
if buf[0] == '*' { | ||
return condFalse | ||
} | ||
etag, remain := scanETag(buf) | ||
if etag == "" { | ||
break | ||
} | ||
buf = remain | ||
} | ||
return condTrue | ||
} | ||
|
||
// ParseTime parses a time header (such as the Date: header), | ||
// trying each of the three formats allowed by HTTP/1.1: | ||
// [TimeFormat], [time.RFC850], and [time.ANSIC]. | ||
func ParseTime(text string) (t time.Time, err error) { | ||
for _, layout := range timeFormats { | ||
t, err = time.Parse(layout, text) | ||
if err == nil { | ||
return | ||
} | ||
} | ||
return | ||
} | ||
|
||
func checkIfMatch(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 | ||
} | ||
im = remain | ||
} | ||
|
||
return condFalse | ||
} | ||
|
||
// 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 "", "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters