Browse Source
Add support for conditional requests (#162)
Add support for conditional requests (#162)
This change pulls in some code copied from net/http's fs.go so that we can support If-Match/If-None-Match requests. This will make it easy to put a caching proxy in front of linx-server instances. Request validation will still happen as long as the proxy can contact the origin, so expiration and deletion will still work as expected under normal circumstances.pull/170/head v2.1.3
mutantmonkey
6 years ago
committed by
Andrei Marcu
3 changed files with 262 additions and 13 deletions
@ -0,0 +1,27 @@ |
|||
Copyright (c) 2009 The Go Authors. All rights reserved. |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions are |
|||
met: |
|||
|
|||
* Redistributions of source code must retain the above copyright |
|||
notice, this list of conditions and the following disclaimer. |
|||
* Redistributions in binary form must reproduce the above |
|||
copyright notice, this list of conditions and the following disclaimer |
|||
in the documentation and/or other materials provided with the |
|||
distribution. |
|||
* Neither the name of Google Inc. nor the names of its |
|||
contributors may be used to endorse or promote products derived from |
|||
this software without specific prior written permission. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,218 @@ |
|||
// 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 |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue