Antonio Mika
·
2025-03-12
fs.go
1// Copyright 2009 The Go Authors.
2
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are
5// met:
6
7// * Redistributions of source code must retain the above copyright
8// notice, this list of conditions and the following disclaimer.
9// * Redistributions in binary form must reproduce the above
10// copyright notice, this list of conditions and the following disclaimer
11// in the documentation and/or other materials provided with the
12// distribution.
13// * Neither the name of Google LLC nor the names of its
14// contributors may be used to endorse or promote products derived from
15// this software without specific prior written permission.
16
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29// HTTP file system request handler
30//
31// Upstream: https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/net/http/fs.go
32// Modifications from upstream:
33// * Deleted everything except checkPreconditions and dependent functions
34// * Added "http" package prefixes
35
36package pgs
37
38import (
39 "net/http"
40 "net/textproto"
41 "strings"
42 "time"
43)
44
45// scanETag determines if a syntactically valid ETag is present at s. If so,
46// the ETag and remaining text after consuming ETag is returned. Otherwise,
47// it returns "", "".
48func scanETag(s string) (etag string, remain string) {
49 s = textproto.TrimString(s)
50 start := 0
51 if strings.HasPrefix(s, "W/") {
52 start = 2
53 }
54 if len(s[start:]) < 2 || s[start] != '"' {
55 return "", ""
56 }
57 // ETag is either W/"text" or "text".
58 // See RFC 7232 2.3.
59 for i := start + 1; i < len(s); i++ {
60 c := s[i]
61 switch {
62 // Character values allowed in ETags.
63 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
64 case c == '"':
65 return s[:i+1], s[i+1:]
66 default:
67 return "", ""
68 }
69 }
70 return "", ""
71}
72
73// etagStrongMatch reports whether a and b match using strong ETag comparison.
74// Assumes a and b are valid ETags.
75func etagStrongMatch(a, b string) bool {
76 return a == b && a != "" && a[0] == '"'
77}
78
79// etagWeakMatch reports whether a and b match using weak ETag comparison.
80// Assumes a and b are valid ETags.
81func etagWeakMatch(a, b string) bool {
82 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
83}
84
85// condResult is the result of an HTTP request precondition check.
86// See https://tools.ietf.org/html/rfc7232 section 3.
87type condResult int
88
89const (
90 condNone condResult = iota
91 condTrue
92 condFalse
93)
94
95func checkIfMatch(w http.ResponseWriter, r *http.Request) condResult {
96 im := r.Header.Get("If-Match")
97 if im == "" {
98 return condNone
99 }
100 for {
101 im = textproto.TrimString(im)
102 if len(im) == 0 {
103 break
104 }
105 if im[0] == ',' {
106 im = im[1:]
107 continue
108 }
109 if im[0] == '*' {
110 return condTrue
111 }
112 etag, remain := scanETag(im)
113 if etag == "" {
114 break
115 }
116 if etagStrongMatch(etag, w.Header().Get("Etag")) {
117 return condTrue
118 }
119 im = remain
120 }
121
122 return condFalse
123}
124
125func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {
126 ius := r.Header.Get("If-Unmodified-Since")
127 if ius == "" || isZeroTime(modtime) {
128 return condNone
129 }
130 t, err := http.ParseTime(ius)
131 if err != nil {
132 return condNone
133 }
134
135 // The Last-Modified header truncates sub-second precision so
136 // the modtime needs to be truncated too.
137 modtime = modtime.Truncate(time.Second)
138 if ret := modtime.Compare(t); ret <= 0 {
139 return condTrue
140 }
141 return condFalse
142}
143
144func checkIfNoneMatch(w http.ResponseWriter, r *http.Request) condResult {
145 inm := r.Header.Get("If-None-Match")
146 if inm == "" {
147 return condNone
148 }
149 buf := inm
150 for {
151 buf = textproto.TrimString(buf)
152 if len(buf) == 0 {
153 break
154 }
155 if buf[0] == ',' {
156 buf = buf[1:]
157 continue
158 }
159 if buf[0] == '*' {
160 return condFalse
161 }
162 etag, remain := scanETag(buf)
163 if etag == "" {
164 break
165 }
166 if etagWeakMatch(etag, w.Header().Get("Etag")) {
167 return condFalse
168 }
169 buf = remain
170 }
171 return condTrue
172}
173
174func checkIfModifiedSince(r *http.Request, modtime time.Time) condResult {
175 if r.Method != "GET" && r.Method != "HEAD" {
176 return condNone
177 }
178 ims := r.Header.Get("If-Modified-Since")
179 if ims == "" || isZeroTime(modtime) {
180 return condNone
181 }
182 t, err := http.ParseTime(ims)
183 if err != nil {
184 return condNone
185 }
186 // The Last-Modified header truncates sub-second precision so
187 // the modtime needs to be truncated too.
188 modtime = modtime.Truncate(time.Second)
189 if ret := modtime.Compare(t); ret <= 0 {
190 return condFalse
191 }
192 return condTrue
193}
194
195func checkIfRange(w http.ResponseWriter, r *http.Request, modtime time.Time) condResult {
196 if r.Method != "GET" && r.Method != "HEAD" {
197 return condNone
198 }
199 ir := r.Header.Get("If-Range")
200 if ir == "" {
201 return condNone
202 }
203 etag, _ := scanETag(ir)
204 if etag != "" {
205 if etagStrongMatch(etag, w.Header().Get("Etag")) {
206 return condTrue
207 } else {
208 return condFalse
209 }
210 }
211 // The If-Range value is typically the ETag value, but it may also be
212 // the modtime date. See golang.org/issue/8367.
213 if modtime.IsZero() {
214 return condFalse
215 }
216 t, err := http.ParseTime(ir)
217 if err != nil {
218 return condFalse
219 }
220 if t.Unix() == modtime.Unix() {
221 return condTrue
222 }
223 return condFalse
224}
225
226var unixEpochTime = time.Unix(0, 0)
227
228// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
229func isZeroTime(t time.Time) bool {
230 return t.IsZero() || t.Equal(unixEpochTime)
231}
232
233func writeNotModified(w http.ResponseWriter) {
234 // RFC 7232 section 4.1:
235 // a sender SHOULD NOT generate representation metadata other than the
236 // above listed fields unless said metadata exists for the purpose of
237 // guiding cache updates (e.g., Last-Modified might be useful if the
238 // response does not have an ETag field).
239 h := w.Header()
240 delete(h, "Content-Type")
241 delete(h, "Content-Length")
242 delete(h, "Content-Encoding")
243 if h.Get("Etag") != "" {
244 delete(h, "Last-Modified")
245 }
246 w.WriteHeader(http.StatusNotModified)
247}
248
249// checkPreconditions evaluates request preconditions and reports whether a precondition
250// resulted in sending http.StatusNotModified or http.StatusPreconditionFailed.
251func checkPreconditions(w http.ResponseWriter, r *http.Request, modtime time.Time) (done bool, rangeHeader string) {
252 // This function carefully follows RFC 7232 section 6.
253 ch := checkIfMatch(w, r)
254 if ch == condNone {
255 ch = checkIfUnmodifiedSince(r, modtime)
256 }
257 if ch == condFalse {
258 w.WriteHeader(http.StatusPreconditionFailed)
259 return true, ""
260 }
261 switch checkIfNoneMatch(w, r) {
262 case condFalse:
263 if r.Method == "GET" || r.Method == "HEAD" {
264 writeNotModified(w)
265 return true, ""
266 } else {
267 w.WriteHeader(http.StatusPreconditionFailed)
268 return true, ""
269 }
270 case condNone:
271 if checkIfModifiedSince(r, modtime) == condFalse {
272 writeNotModified(w)
273 return true, ""
274 }
275 }
276
277 rangeHeader = r.Header.Get("Range")
278 if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
279 rangeHeader = ""
280 }
281 return false, rangeHeader
282}