Skip to content

Commit

Permalink
include checkPrecoditions check
Browse files Browse the repository at this point in the history
  • Loading branch information
anik120 committed Jan 31, 2025
1 parent fc80fae commit aa78a6c
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
224 changes: 224 additions & 0 deletions catalogd/internal/storage/http_precoditions_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// 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.
//
// Source: Originally from Go's net/http/fs.go
// https://cs.opensource.google/go/go/+/master:src/net/http/fs.go

package storage

import (
"net/http"
"net/textproto"
"strings"
"time"
)

type condResult int

const (
condNone condResult = iota
condTrue
condFalse
)

// 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) {

Check failure on line 27 in catalogd/internal/storage/http_precoditions_check.go

View workflow job for this annotation

GitHub Actions / lint

named return "done" with type "bool" found (nonamedreturns)
// This function carefully follows RFC 7232 section 6.
ch := checkIfMatch(r)
if ch == condNone {
ch = checkIfUnmodifiedSince(r, modtime)
}
if ch == condFalse {
w.WriteHeader(http.StatusPreconditionFailed)
return true
}
switch checkIfNoneMatch(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, w, modtime) == condFalse {
writeNotModified(w)
return true
}
}
return false
}

func checkIfModifiedSince(r *http.Request, w http.ResponseWriter, modtime time.Time) condResult {
ims := r.Header.Get("If-Modified-Since")
if ims == "" || isZeroTime(modtime) {
return condTrue
}
t, err := ParseTime(ims)
if err != nil {
httpError(w, err)
return condNone
}
// The Last-Modified header truncates sub-second precision so
// the modtime needs to be truncated too.
modtime = modtime.Truncate(time.Second)
if modtime.Compare(t) <= 0 {
return condFalse
}
return condTrue
}

func checkIfUnmodifiedSince(r *http.Request, modtime time.Time) condResult {
ius := r.Header.Get("If-Unmodified-Since")
if ius == "" || isZeroTime(modtime) {
return condNone
}
t, err := ParseTime(ius)
if err != nil {
return condNone
}

// The Last-Modified header truncates sub-second precision so
// the modtime needs to be truncated too.
modtime = modtime.Truncate(time.Second)
if ret := modtime.Compare(t); ret <= 0 {
return condTrue
}
return condFalse
}

// TimeFormat is the time format to use when generating times in HTTP
// headers. It is like [time.RFC1123] but hard-codes GMT as the time
// zone. The time being formatted must be in UTC for Format to
// generate the correct format.
//
// For parsing this time format, see [ParseTime].
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"

var (
unixEpochTime = time.Unix(0, 0)
timeFormats = []string{
TimeFormat,
time.RFC850,
time.ANSIC,
}
)

// 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 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")
delete(h, "Content-Encoding")
if h.Get("Etag") != "" {
delete(h, "Last-Modified")
}
w.WriteHeader(http.StatusNotModified)
}

func checkIfNoneMatch(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:]
continue
}
if buf[0] == '*' {
return condFalse
}
etag, remain := scanETag(buf)
if etag == "" {
break
}
buf = remain
}
return condTrue
}

// ParseTime parses a time header (such as the Date: header),
// trying each of the three formats allowed by HTTP/1.1:
// [TimeFormat], [time.RFC850], and [time.ANSIC].
func ParseTime(text string) (t time.Time, err error) {

Check failure on line 161 in catalogd/internal/storage/http_precoditions_check.go

View workflow job for this annotation

GitHub Actions / lint

named return "t" with type "time.Time" found (nonamedreturns)
for _, layout := range timeFormats {
t, err = time.Parse(layout, text)
if err == nil {
return
}
}
return
}

func checkIfMatch(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
}
im = remain
}

return condFalse
}

// 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) {

Check failure on line 201 in catalogd/internal/storage/http_precoditions_check.go

View workflow job for this annotation

GitHub Actions / lint

named return "etag" with type "string" found (nonamedreturns)
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 "", ""
}
6 changes: 6 additions & 0 deletions catalogd/internal/storage/localdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ func (s *LocalDirV1) handleV1Query(w http.ResponseWriter, r *http.Request) {
}
defer catalogFile.Close()

w.Header().Set("Last-Modified", catalogStat.ModTime().UTC().Format(TimeFormat))
if checkPreconditions(w, r, catalogStat.ModTime()) {
w.WriteHeader(http.StatusNotModified)
return
}

schema := r.URL.Query().Get("schema")
pkg := r.URL.Query().Get("package")
name := r.URL.Query().Get("name")
Expand Down

0 comments on commit aa78a6c

Please sign in to comment.