diff --git a/fresh.go b/fresh.go new file mode 100644 index 0000000..139dc02 --- /dev/null +++ b/fresh.go @@ -0,0 +1,130 @@ +// MIT License + +// Copyright (c) 2021 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package elton + +import ( + "bytes" + "net/http" + "regexp" + "time" +) + +var noCacheReg = regexp.MustCompile(`(?:^|,)\s*?no-cache\s*?(?:,|$)`) + +var weekTagPrefix = []byte("W/") + +func parseTokenList(buf []byte) [][]byte { + end := 0 + start := 0 + count := len(buf) + list := make([][]byte, 0) + for index := 0; index < count; index++ { + switch int(buf[index]) { + // 空格 + case 0x20: + if start == end { + end = index + 1 + start = end + } + // , 号 + case 0x2c: + list = append(list, buf[start:end]) + end = index + 1 + start = end + default: + end = index + 1 + } + } + list = append(list, buf[start:end]) + return list +} + +func parseHTTPDate(date string) int64 { + t, err := time.Parse(time.RFC1123, date) + if err != nil { + return 0 + } + return t.Unix() +} + +// isFresh returns true if the data is fresh +func isFresh(modifiedSince, noneMatch, cacheControl, lastModified, etag []byte) bool { + if len(modifiedSince) == 0 && len(noneMatch) == 0 { + return false + } + if len(cacheControl) != 0 && noCacheReg.Match(cacheControl) { + return false + } + // if none match + if len(noneMatch) != 0 && (len(noneMatch) != 1 || noneMatch[0] != byte('*')) { + if len(etag) == 0 { + return false + } + matches := parseTokenList(noneMatch) + etagStale := true + for _, match := range matches { + if bytes.Equal(match, etag) { + etagStale = false + break + } + if bytes.HasPrefix(match, weekTagPrefix) && bytes.Equal(match[2:], etag) { + etagStale = false + break + } + if bytes.HasPrefix(etag, weekTagPrefix) && bytes.Equal(etag[2:], match) { + etagStale = false + break + } + } + if etagStale { + return false + } + } + // if modified since + if len(modifiedSince) != 0 { + if len(lastModified) == 0 { + return false + } + lastModifiedUnix := parseHTTPDate(string(lastModified)) + modifiedSinceUnix := parseHTTPDate(string(modifiedSince)) + if lastModifiedUnix == 0 || modifiedSinceUnix == 0 { + return false + } + if modifiedSinceUnix < lastModifiedUnix { + return false + } + } + return true +} + +// Fresh returns fresh status by judget request header and response header +func Fresh(reqHeader http.Header, resHeader http.Header) bool { + modifiedSince := []byte(reqHeader.Get(HeaderIfModifiedSince)) + noneMatch := []byte(reqHeader.Get(HeaderIfNoneMatch)) + cacheControl := []byte(reqHeader.Get(HeaderCacheControl)) + + lastModified := []byte(resHeader.Get(HeaderLastModified)) + etag := []byte(resHeader.Get(HeaderETag)) + + return isFresh(modifiedSince, noneMatch, cacheControl, lastModified, etag) +} diff --git a/fresh_test.go b/fresh_test.go new file mode 100644 index 0000000..cb8402c --- /dev/null +++ b/fresh_test.go @@ -0,0 +1,193 @@ +// MIT License + +// Copyright (c) 2021 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package elton + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func createRequestHeader(modifiedSince, noneMatch, cacheControl string) http.Header { + req := httptest.NewRequest("GET", "/users/me", nil) + header := req.Header + if modifiedSince != "" { + header.Set(HeaderIfModifiedSince, modifiedSince) + } + + if noneMatch != "" { + header.Set(HeaderIfNoneMatch, noneMatch) + } + + if cacheControl != "" { + header.Set(HeaderCacheControl, cacheControl) + } + return header +} + +func createResponseHeader(lastModified, eTag string) http.Header { + resp := httptest.NewRecorder() + header := resp.Header() + if lastModified != "" { + header.Set(HeaderLastModified, lastModified) + } + + if eTag != "" { + header.Set(HeaderETag, eTag) + } + return header +} +func TestFresh(t *testing.T) { + assert := assert.New(t) + // when a non-conditional GET is performed + reqHeader := createRequestHeader("", "", "") + resHeader := createResponseHeader("", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when ETags match + reqHeader = createRequestHeader("", "\"foo\"", "") + + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + reqHeader = createRequestHeader("", "W/\"foo\"", "") + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + reqHeader = createRequestHeader("", "\"foo\"", "") + resHeader = createResponseHeader("", "W/\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // when ETags mismatch + reqHeader = createRequestHeader("", "\"foo\"", "") + resHeader = createResponseHeader("", "\"bar\"") + assert.False(Fresh(reqHeader, resHeader)) + + // when at least one matches + reqHeader = createRequestHeader("", " \"bar\" , \"foo\"", "") + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // when eTag is missing + reqHeader = createRequestHeader("", "\"foo\"", "") + resHeader = createResponseHeader("", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when ETag is weak + reqHeader = createRequestHeader("", "W/\"foo\"", "") + resHeader = createResponseHeader("", "W/\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // when ETag is strong + reqHeader = createRequestHeader("", "\"foo\"", "") + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // weak eTag + resHeader = createResponseHeader("", "W/\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // when * is given + reqHeader = createRequestHeader("", "*", "") + resHeader = createResponseHeader("", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + reqHeader = createRequestHeader("", "*, \"bar\"", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when modified since the date + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 01:00:00 GMT", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when unmodified since the date + reqHeader = createRequestHeader("Sat, 01 Jan 2000 01:00:00 GMT", "", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "") + assert.True(Fresh(reqHeader, resHeader)) + + // when Last-Modified is missing + reqHeader = createRequestHeader("Sat, 01 Jan 2000 01:00:00 GMT", "", "") + resHeader = createResponseHeader("", "") + assert.False(Fresh(reqHeader, resHeader)) + + // with invalid If-Modified-Since date + reqHeader = createRequestHeader("foo", "", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "") + assert.False(Fresh(reqHeader, resHeader)) + + // with invalid Last-Modified date + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "", "") + resHeader = createResponseHeader("foo", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when requested with If-Modified-Since and If-None-Match + + // both match + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"") + assert.True(Fresh(reqHeader, resHeader)) + + // when only ETag matches + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 01:00:00 GMT", "\"foo\"") + assert.False(Fresh(reqHeader, resHeader)) + + // when only Last-Modified matches + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"bar\"") + assert.False(Fresh(reqHeader, resHeader)) + + // when none match + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"", "") + resHeader = createResponseHeader("Sat, 01 Jan 2000 01:00:00 GMT", "\"bar\"") + assert.False(Fresh(reqHeader, resHeader)) + + // when requested with Cache-Control: no-cache + reqHeader = createRequestHeader("", "", "no-cache") + resHeader = createResponseHeader("", "") + assert.False(Fresh(reqHeader, resHeader)) + + // when ETags match + reqHeader = createRequestHeader("", "\"foo\"", "no-cache") + resHeader = createResponseHeader("", "\"foo\"") + assert.False(Fresh(reqHeader, resHeader)) + + // when unmodified since the date + reqHeader = createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "", "no-cache") + resHeader = createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"") + assert.False(Fresh(reqHeader, resHeader)) +} + +func BenchmarkFresh(b *testing.B) { + b.ResetTimer() + reqHeader := createRequestHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"", "") + resHeader := createResponseHeader("Sat, 01 Jan 2000 00:00:00 GMT", "\"foo\"") + for i := 0; i < b.N; i++ { + Fresh(reqHeader, resHeader) + } +} diff --git a/go.mod b/go.mod index c3e65c4..c85a1f2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.8.0 - github.com/vicanso/fresh v1.0.0 github.com/vicanso/hes v0.3.9 github.com/vicanso/intranet-ip v0.0.1 github.com/vicanso/keygrip v1.2.1 diff --git a/go.sum b/go.sum index 39c88fd..b493177 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -13,8 +14,6 @@ github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/vicanso/fresh v1.0.0 h1:u3ykbW6SYW5CbI6rx1lZCVfWKVIptvL6/2KOnrTKsTY= -github.com/vicanso/fresh v1.0.0/go.mod h1:gr1RKSFxQ1OnQHzUMBHCigifni7KrXveJjWCTlPjICA= github.com/vicanso/hes v0.3.9 h1:IO21yElX6Xp3w+Lc1O2QIySrJj2jEhnl5dWbqbDYunc= github.com/vicanso/hes v0.3.9/go.mod h1:B0l1NIQM/nYw7owAd+hyHuNnAD8Nsx0T6duhVxmXUBY= github.com/vicanso/intranet-ip v0.0.1 h1:cYS+mExFsKqewWSuHtFwAqw/CO66GsheB/P1BPmSTx0= diff --git a/middleware/fresh.go b/middleware/fresh.go index 39fc223..924f04f 100644 --- a/middleware/fresh.go +++ b/middleware/fresh.go @@ -26,7 +26,6 @@ import ( "net/http" "github.com/vicanso/elton" - "github.com/vicanso/fresh" ) type ( @@ -77,7 +76,7 @@ func NewFresh(config FreshConfig) elton.Handler { } // 304的处理 - if fresh.Fresh(c.Request.Header, c.Header()) { + if elton.Fresh(c.Request.Header, c.Header()) { c.NotModified() } return