Skip to content

Commit

Permalink
refactor: support frech check function
Browse files Browse the repository at this point in the history
  • Loading branch information
vicanso committed Jun 3, 2021
1 parent b3fe9a9 commit 94a34fb
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 5 deletions.
130 changes: 130 additions & 0 deletions fresh.go
Original file line number Diff line number Diff line change
@@ -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)
}
193 changes: 193 additions & 0 deletions fresh_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
3 changes: 1 addition & 2 deletions middleware/fresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"net/http"

"github.com/vicanso/elton"
"github.com/vicanso/fresh"
)

type (
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 94a34fb

Please sign in to comment.