Skip to content

Commit eee6650

Browse files
committed
Add precondition.go.
1 parent 1666abf commit eee6650

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

webserver/precondition.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package webserver
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
)
7+
8+
// This is partly based on the Go standard library file net/http/fs.go.
9+
10+
// scanETag determines if a syntactically valid ETag is present at s. If so,
11+
// the ETag and remaining text after consuming ETag is returned. Otherwise,
12+
// it returns "", "".
13+
func scanETag(s string) (etag string, remain string) {
14+
s = strings.TrimLeft(s, " \t\n\r")
15+
start := 0
16+
if strings.HasPrefix(s, "W/") {
17+
start = 2
18+
}
19+
if len(s[start:]) < 2 || s[start] != '"' {
20+
return "", ""
21+
}
22+
// ETag is either W/"text" or "text".
23+
// See RFC 7232 2.3.
24+
for i := start + 1; i < len(s); i++ {
25+
c := s[i]
26+
switch {
27+
// Character values allowed in ETags.
28+
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
29+
case c == '"':
30+
return s[:i+1], s[i+1:]
31+
default:
32+
return "", ""
33+
}
34+
}
35+
return "", ""
36+
}
37+
38+
// etagMatch returns whether the given eTag matches an IM/INM header value.
39+
// The empty string in etag is interpreted as a non-existent object.
40+
func etagMatch(etag, header string) bool {
41+
if header == "" {
42+
return false
43+
}
44+
if header == etag {
45+
return true
46+
}
47+
48+
for {
49+
header = strings.TrimLeft(header, " \t\n\r")
50+
if len(header) == 0 {
51+
break
52+
}
53+
if header[0] == ',' {
54+
header = header[1:]
55+
continue
56+
}
57+
if header[0] == '*' {
58+
return etag != ""
59+
}
60+
e, remain := scanETag(header)
61+
if e == "" {
62+
break
63+
}
64+
if e == etag {
65+
return true
66+
}
67+
header = remain
68+
}
69+
70+
return false
71+
}
72+
73+
func writeNotModified(w http.ResponseWriter) {
74+
// RFC 7232 section 4.1:
75+
// a sender SHOULD NOT generate representation metadata other than the
76+
// above listed fields unless said metadata exists for the purpose of
77+
// guiding cache updates (e.g., Last-Modified might be useful if the
78+
// response does not have an ETag field).
79+
h := w.Header()
80+
delete(h, "Content-Type")
81+
delete(h, "Content-Length")
82+
delete(h, "Content-Encoding")
83+
if h.Get("Etag") != "" {
84+
delete(h, "Last-Modified")
85+
}
86+
w.WriteHeader(http.StatusNotModified)
87+
}
88+
89+
// checkPreconditions evaluates request preconditions.
90+
// It interprets an empty etag as a non-existent object.
91+
func checkPreconditions(w http.ResponseWriter, r *http.Request, etag string) (done bool) {
92+
// RFC 7232 section 6.
93+
im := r.Header.Get("If-Match")
94+
if im != "" && !etagMatch(etag, im) {
95+
w.WriteHeader(http.StatusPreconditionFailed)
96+
return true
97+
}
98+
inm := r.Header.Get("If-None-Match")
99+
if inm != "" && etagMatch(etag, inm) {
100+
if r.Method == "GET" || r.Method == "HEAD" {
101+
writeNotModified(w)
102+
return true
103+
} else {
104+
w.WriteHeader(http.StatusPreconditionFailed)
105+
return true
106+
}
107+
}
108+
109+
return false
110+
}

webserver/precondition_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package webserver
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
type testWriter struct {
9+
statusCode int
10+
}
11+
12+
func (w *testWriter) Header() http.Header {
13+
return nil
14+
}
15+
16+
func (w *testWriter) Write(buf []byte) (int, error) {
17+
return len(buf), nil
18+
}
19+
20+
func (w *testWriter) WriteHeader(statusCode int) {
21+
if w.statusCode != 0 {
22+
panic("WriteHeader called twice")
23+
}
24+
w.statusCode = statusCode
25+
}
26+
27+
func TestEtagMatch(t *testing.T) {
28+
type tst struct {
29+
etag, header string
30+
}
31+
32+
var match = []tst{
33+
{`"foo"`, `"foo"`},
34+
{`"foo"`, ` "foo"`},
35+
{`"foo"`, `"foo" `},
36+
{`"foo"`, ` "foo" `},
37+
{`"foo"`, `"foo", "bar"`},
38+
{`"foo"`, `"bar", "foo"`},
39+
{`W/"foo"`, `W/"foo"`},
40+
}
41+
42+
var mismatch = []tst{
43+
{``, ``},
44+
{``, `*`},
45+
{``, `"foo"`},
46+
{`"foo"`, ``},
47+
{`"foo"`, `"bar"`},
48+
{`"foo"`, `"bar", "baz"`},
49+
{`"foo"`, `"baz", "bar"`},
50+
{`"foo"`, `W/"foo"`},
51+
{`W/"foo"`, `"foo"`},
52+
}
53+
54+
for _, tst := range match {
55+
m := etagMatch(tst.etag, tst.header)
56+
if !m {
57+
t.Errorf("%#v %#v: got %v, expected true",
58+
tst.etag, tst.header, m,
59+
)
60+
}
61+
}
62+
63+
for _, tst := range mismatch {
64+
m := etagMatch(tst.etag, tst.header)
65+
if m {
66+
t.Errorf("%#v %#v: got %v, expected false",
67+
tst.etag, tst.header, m,
68+
)
69+
}
70+
}
71+
}
72+
73+
func TestCheckPreconditions(t *testing.T) {
74+
var tests = []struct {
75+
method, etag, im, inm string
76+
result int
77+
}{
78+
{"GET", ``, ``, ``, 0},
79+
{"GET", ``, `*`, ``, 412},
80+
{"GET", ``, ``, `*`, 0},
81+
{"POST", ``, `*`, ``, 412},
82+
{"POST", ``, ``, `*`, 0},
83+
{"GET", `"123"`, ``, ``, 0},
84+
{"GET", `"123"`, `"123"`, ``, 0},
85+
{"GET", `"123"`, `"124"`, ``, 412},
86+
{"POST", `"123"`, `"124"`, ``, 412},
87+
{"GET", `"123"`, `*`, ``, 0},
88+
{"GET", `"123"`, ``, `"123"`, 304},
89+
{"POST", `"123"`, ``, `"123"`, 412},
90+
{"GET", `"123"`, ``, `"124"`, 0},
91+
{"GET", `"123"`, ``, `*`, 304},
92+
}
93+
94+
for _, tst := range tests {
95+
var w testWriter
96+
h := make(http.Header)
97+
if tst.im != "" {
98+
h.Set("If-Match", tst.im)
99+
}
100+
if tst.inm != "" {
101+
h.Set("If-None-Match", tst.inm)
102+
}
103+
r := http.Request{
104+
Method: tst.method,
105+
Header: h,
106+
}
107+
done := checkPreconditions(&w, &r, tst.etag)
108+
if done != (tst.result != 0) || w.statusCode != tst.result {
109+
t.Errorf("%#v %#v %#v: got %v, expected %v",
110+
tst.etag, tst.im, tst.inm,
111+
w.statusCode, tst.result)
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)