Skip to content

Commit aa78a6c

Browse files
committed
include checkPrecoditions check
1 parent fc80fae commit aa78a6c

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright 2009 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
//
5+
// Source: Originally from Go's net/http/fs.go
6+
// https://cs.opensource.google/go/go/+/master:src/net/http/fs.go
7+
8+
package storage
9+
10+
import (
11+
"net/http"
12+
"net/textproto"
13+
"strings"
14+
"time"
15+
)
16+
17+
type condResult int
18+
19+
const (
20+
condNone condResult = iota
21+
condTrue
22+
condFalse
23+
)
24+
25+
// checkPreconditions evaluates request preconditions and reports whether a precondition
26+
// resulted in sending StatusNotModified or StatusPreconditionFailed.
27+
func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool) {
28+
// This function carefully follows RFC 7232 section 6.
29+
ch := checkIfMatch(r)
30+
if ch == condNone {
31+
ch = checkIfUnmodifiedSince(r, modtime)
32+
}
33+
if ch == condFalse {
34+
w.WriteHeader(http.StatusPreconditionFailed)
35+
return true
36+
}
37+
switch checkIfNoneMatch(r) {
38+
case condFalse:
39+
if r.Method == "GET" || r.Method == "HEAD" {
40+
writeNotModified(w)
41+
return true
42+
} else {
43+
w.WriteHeader(http.StatusPreconditionFailed)
44+
return true
45+
}
46+
case condNone:
47+
if checkIfModifiedSince(r, w, modtime) == condFalse {
48+
writeNotModified(w)
49+
return true
50+
}
51+
}
52+
return false
53+
}
54+
55+
func checkIfModifiedSince(r *http.Request, w http.ResponseWriter, modtime time.Time) condResult {
56+
ims := r.Header.Get("If-Modified-Since")
57+
if ims == "" || isZeroTime(modtime) {
58+
return condTrue
59+
}
60+
t, err := ParseTime(ims)
61+
if err != nil {
62+
httpError(w, err)
63+
return condNone
64+
}
65+
// The Last-Modified header truncates sub-second precision so
66+
// the modtime needs to be truncated too.
67+
modtime = modtime.Truncate(time.Second)
68+
if modtime.Compare(t) <= 0 {
69+
return condFalse
70+
}
71+
return condTrue
72+
}
73+
74+
func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {
75+
ius := r.Header.Get("If-Unmodified-Since")
76+
if ius == "" || isZeroTime(modtime) {
77+
return condNone
78+
}
79+
t, err := ParseTime(ius)
80+
if err != nil {
81+
return condNone
82+
}
83+
84+
// The Last-Modified header truncates sub-second precision so
85+
// the modtime needs to be truncated too.
86+
modtime = modtime.Truncate(time.Second)
87+
if ret := modtime.Compare(t); ret <= 0 {
88+
return condTrue
89+
}
90+
return condFalse
91+
}
92+
93+
// TimeFormat is the time format to use when generating times in HTTP
94+
// headers. It is like [time.RFC1123] but hard-codes GMT as the time
95+
// zone. The time being formatted must be in UTC for Format to
96+
// generate the correct format.
97+
//
98+
// For parsing this time format, see [ParseTime].
99+
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
100+
101+
var (
102+
unixEpochTime = time.Unix(0, 0)
103+
timeFormats = []string{
104+
TimeFormat,
105+
time.RFC850,
106+
time.ANSIC,
107+
}
108+
)
109+
110+
// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
111+
func isZeroTime(t time.Time) bool {
112+
return t.IsZero() || t.Equal(unixEpochTime)
113+
}
114+
115+
func writeNotModified(w http.ResponseWriter) {
116+
// RFC 7232 section 4.1:
117+
// a sender SHOULD NOT generate representation metadata other than the
118+
// above listed fields unless said metadata exists for the purpose of
119+
// guiding cache updates (e.g., Last-Modified might be useful if the
120+
// response does not have an ETag field).
121+
h := w.Header()
122+
delete(h, "Content-Type")
123+
delete(h, "Content-Length")
124+
delete(h, "Content-Encoding")
125+
if h.Get("Etag") != "" {
126+
delete(h, "Last-Modified")
127+
}
128+
w.WriteHeader(http.StatusNotModified)
129+
}
130+
131+
func checkIfNoneMatch(r *http.Request) condResult {
132+
inm := r.Header.Get("If-None-Match")
133+
if inm == "" {
134+
return condNone
135+
}
136+
buf := inm
137+
for {
138+
buf = textproto.TrimString(buf)
139+
if len(buf) == 0 {
140+
break
141+
}
142+
if buf[0] == ',' {
143+
buf = buf[1:]
144+
continue
145+
}
146+
if buf[0] == '*' {
147+
return condFalse
148+
}
149+
etag, remain := scanETag(buf)
150+
if etag == "" {
151+
break
152+
}
153+
buf = remain
154+
}
155+
return condTrue
156+
}
157+
158+
// ParseTime parses a time header (such as the Date: header),
159+
// trying each of the three formats allowed by HTTP/1.1:
160+
// [TimeFormat], [time.RFC850], and [time.ANSIC].
161+
func ParseTime(text string) (t time.Time, err error) {
162+
for _, layout := range timeFormats {
163+
t, err = time.Parse(layout, text)
164+
if err == nil {
165+
return
166+
}
167+
}
168+
return
169+
}
170+
171+
func checkIfMatch(r *http.Request) condResult {
172+
im := r.Header.Get("If-Match")
173+
if im == "" {
174+
return condNone
175+
}
176+
for {
177+
im = textproto.TrimString(im)
178+
if len(im) == 0 {
179+
break
180+
}
181+
if im[0] == ',' {
182+
im = im[1:]
183+
continue
184+
}
185+
if im[0] == '*' {
186+
return condTrue
187+
}
188+
etag, remain := scanETag(im)
189+
if etag == "" {
190+
break
191+
}
192+
im = remain
193+
}
194+
195+
return condFalse
196+
}
197+
198+
// scanETag determines if a syntactically valid ETag is present at s. If so,
199+
// the ETag and remaining text after consuming ETag is returned. Otherwise,
200+
// it returns "", "".
201+
func scanETag(s string) (etag string, remain string) {
202+
s = textproto.TrimString(s)
203+
start := 0
204+
if strings.HasPrefix(s, "W/") {
205+
start = 2
206+
}
207+
if len(s[start:]) < 2 || s[start] != '"' {
208+
return "", ""
209+
}
210+
// ETag is either W/"text" or "text".
211+
// See RFC 7232 2.3.
212+
for i := start + 1; i < len(s); i++ {
213+
c := s[i]
214+
switch {
215+
// Character values allowed in ETags.
216+
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
217+
case c == '"':
218+
return s[:i+1], s[i+1:]
219+
default:
220+
return "", ""
221+
}
222+
}
223+
return "", ""
224+
}

catalogd/internal/storage/localdir.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ func (s *LocalDirV1) handleV1Query(w http.ResponseWriter, r *http.Request) {
217217
}
218218
defer catalogFile.Close()
219219

220+
w.Header().Set("Last-Modified", catalogStat.ModTime().UTC().Format(TimeFormat))
221+
if checkPreconditions(w, r, catalogStat.ModTime()) {
222+
w.WriteHeader(http.StatusNotModified)
223+
return
224+
}
225+
220226
schema := r.URL.Query().Get("schema")
221227
pkg := r.URL.Query().Get("package")
222228
name := r.URL.Query().Get("name")

0 commit comments

Comments
 (0)