repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / apps / pgs
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}