pax_global_header00006660000000000000000000000064146425221230014513gustar00rootroot0000000000000052 comment=823acc44a18c54ad5ea2d64ca41fb72a226a14ad golang-github-lestrrat-go-httpcc-1.0.1/000077500000000000000000000000001464252212300177655ustar00rootroot00000000000000golang-github-lestrrat-go-httpcc-1.0.1/.github/000077500000000000000000000000001464252212300213255ustar00rootroot00000000000000golang-github-lestrrat-go-httpcc-1.0.1/.github/workflows/000077500000000000000000000000001464252212300233625ustar00rootroot00000000000000golang-github-lestrrat-go-httpcc-1.0.1/.github/workflows/ci.yml000066400000000000000000000017151464252212300245040ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go: [ '1.15', '1.14' ] name: Go ${{ matrix.go }} test steps: - name: Checkout repository uses: actions/checkout@v2 - name: Install Go stable version if: matrix.go != 'tip' uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Install Go tip if: matrix.go == 'tip' run: | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip cd $HOME/gotip/src ./make.bash echo "::set-env name=GOROOT::$HOME/gotip" echo "::add-path::$HOME/gotip/bin" echo "::add-path::$(go env GOPATH)/bin" - name: Test run: go test -v -race ./... - name: Upload code coverage to codecov if: matrix.go == '1.15' uses: codecov/codecov-action@v1 with: file: ./coverage.out golang-github-lestrrat-go-httpcc-1.0.1/.github/workflows/lint.yml000066400000000000000000000003451464252212300250550ustar00rootroot00000000000000name: lint on: [push, pull_request] jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: golangci/golangci-lint-action@v2 with: version: v1.34.1 golang-github-lestrrat-go-httpcc-1.0.1/.gitignore000066400000000000000000000004151464252212300217550ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ golang-github-lestrrat-go-httpcc-1.0.1/LICENSE000066400000000000000000000020541464252212300207730ustar00rootroot00000000000000MIT License Copyright (c) 2020 lestrrat-go 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. golang-github-lestrrat-go-httpcc-1.0.1/README.md000066400000000000000000000016061464252212300212470ustar00rootroot00000000000000httpcc ====== Parses HTTP/1.1 Cache-Control header, and returns a struct that is convenient for the end-user to do what they will with. # Parsing the HTTP Request ```go dir, err := httpcc.ParseRequest(req.Header.Get(`Cache-Control`)) // dir.MaxAge() uint64, bool // dir.MaxStale() uint64, bool // dir.MinFresh() uint64, bool // dir.NoCache() bool // dir.NoStore() bool // dir.NoTransform() bool // dir.OnlyIfCached() bool // dir.Extensions() map[string]string ``` # Parsing the HTTP Response ```go directives, err := httpcc.ParseResponse(res.Header.Get(`Cache-Control`)) // dir.MaxAge() uint64, bool // dir.MustRevalidate() bool // dir.NoCache() []string // dir.NoStore() bool // dir.NoTransform() bool // dir.Public() bool // dir.Private() bool // dir.SMaxAge() uint64, bool // dir.Extensions() map[string]string ``` golang-github-lestrrat-go-httpcc-1.0.1/directives.go000066400000000000000000000041551464252212300224620ustar00rootroot00000000000000package httpcc type RequestDirective struct { maxAge *uint64 maxStale *uint64 minFresh *uint64 noCache bool noStore bool noTransform bool onlyIfCached bool extensions map[string]string } func (d *RequestDirective) MaxAge() (uint64, bool) { if v := d.maxAge; v != nil { return *v, true } return 0, false } func (d *RequestDirective) MaxStale() (uint64, bool) { if v := d.maxStale; v != nil { return *v, true } return 0, false } func (d *RequestDirective) MinFresh() (uint64, bool) { if v := d.minFresh; v != nil { return *v, true } return 0, false } func (d *RequestDirective) NoCache() bool { return d.noCache } func (d *RequestDirective) NoStore() bool { return d.noStore } func (d *RequestDirective) NoTransform() bool { return d.noTransform } func (d *RequestDirective) OnlyIfCached() bool { return d.onlyIfCached } func (d *RequestDirective) Extensions() map[string]string { return d.extensions } func (d *RequestDirective) Extension(s string) string { return d.extensions[s] } type ResponseDirective struct { maxAge *uint64 noCache []string noStore bool noTransform bool public bool private []string proxyRevalidate bool sMaxAge *uint64 extensions map[string]string } func (d *ResponseDirective) MaxAge() (uint64, bool) { if v := d.maxAge; v != nil { return *v, true } return 0, false } func (d *ResponseDirective) NoCache() []string { return d.noCache } func (d *ResponseDirective) NoStore() bool { return d.noStore } func (d *ResponseDirective) NoTransform() bool { return d.noTransform } func (d *ResponseDirective) Public() bool { return d.public } func (d *ResponseDirective) Private() []string { return d.private } func (d *ResponseDirective) ProxyRevalidate() bool { return d.proxyRevalidate } func (d *ResponseDirective) SMaxAge() (uint64, bool) { if v := d.sMaxAge; v != nil { return *v, true } return 0, false } func (d *ResponseDirective) Extensions() map[string]string { return d.extensions } func (d *ResponseDirective) Extension(s string) string { return d.extensions[s] } golang-github-lestrrat-go-httpcc-1.0.1/go.mod000066400000000000000000000001321464252212300210670ustar00rootroot00000000000000module github.com/lestrrat-go/httpcc go 1.16 require github.com/stretchr/testify v1.7.1 golang-github-lestrrat-go-httpcc-1.0.1/go.sum000066400000000000000000000020001464252212300211100ustar00rootroot00000000000000github.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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-lestrrat-go-httpcc-1.0.1/httpcc.go000066400000000000000000000172251464252212300216100ustar00rootroot00000000000000package httpcc import ( "bufio" "fmt" "strconv" "strings" "unicode/utf8" ) const ( // Request Cache-Control directives MaxAge = "max-age" // used in response as well MaxStale = "max-stale" MinFresh = "min-fresh" NoCache = "no-cache" // used in response as well NoStore = "no-store" // used in response as well NoTransform = "no-transform" // used in response as well OnlyIfCached = "only-if-cached" // Response Cache-Control directive MustRevalidate = "must-revalidate" Public = "public" Private = "private" ProxyRevalidate = "proxy-revalidate" SMaxAge = "s-maxage" ) type TokenPair struct { Name string Value string } type TokenValuePolicy int const ( NoArgument TokenValuePolicy = iota TokenOnly QuotedStringOnly AnyTokenValue ) type directiveValidator interface { Validate(string) TokenValuePolicy } type directiveValidatorFn func(string) TokenValuePolicy func (fn directiveValidatorFn) Validate(ccd string) TokenValuePolicy { return fn(ccd) } func responseDirectiveValidator(s string) TokenValuePolicy { switch s { case MustRevalidate, NoStore, NoTransform, Public, ProxyRevalidate: return NoArgument case NoCache, Private: return QuotedStringOnly case MaxAge, SMaxAge: return TokenOnly default: return AnyTokenValue } } func requestDirectiveValidator(s string) TokenValuePolicy { switch s { case MaxAge, MaxStale, MinFresh: return TokenOnly case NoCache, NoStore, NoTransform, OnlyIfCached: return NoArgument default: return AnyTokenValue } } // ParseRequestDirective parses a single token. func ParseRequestDirective(s string) (*TokenPair, error) { return parseDirective(s, directiveValidatorFn(requestDirectiveValidator)) } func ParseResponseDirective(s string) (*TokenPair, error) { return parseDirective(s, directiveValidatorFn(responseDirectiveValidator)) } func parseDirective(s string, ccd directiveValidator) (*TokenPair, error) { s = strings.TrimSpace(s) i := strings.IndexByte(s, '=') if i == -1 { return &TokenPair{Name: s}, nil } pair := &TokenPair{Name: strings.TrimSpace(s[:i])} if len(s) <= i { // `key=` feels like it's a parse error, but it's HTTP... // for now, return as if nothing happened. return pair, nil } v := strings.TrimSpace(s[i+1:]) switch ccd.Validate(pair.Name) { case TokenOnly: if v[0] == '"' { return nil, fmt.Errorf(`invalid value for %s (quoted string not allowed)`, pair.Name) } case QuotedStringOnly: // quoted-string only if v[0] != '"' { return nil, fmt.Errorf(`invalid value for %s (bare token not allowed)`, pair.Name) } tmp, err := strconv.Unquote(v) if err != nil { return nil, fmt.Errorf(`malformed quoted string in token`) } v = tmp case AnyTokenValue: if v[0] == '"' { tmp, err := strconv.Unquote(v) if err != nil { return nil, fmt.Errorf(`malformed quoted string in token`) } v = tmp } case NoArgument: if len(v) > 0 { return nil, fmt.Errorf(`received argument to directive %s`, pair.Name) } } pair.Value = v return pair, nil } func ParseResponseDirectives(s string) ([]*TokenPair, error) { return parseDirectives(s, ParseResponseDirective) } func ParseRequestDirectives(s string) ([]*TokenPair, error) { return parseDirectives(s, ParseRequestDirective) } func parseDirectives(s string, p func(string) (*TokenPair, error)) ([]*TokenPair, error) { scanner := bufio.NewScanner(strings.NewReader(s)) scanner.Split(scanCommaSeparatedWords) var tokens []*TokenPair for scanner.Scan() { tok, err := p(scanner.Text()) if err != nil { return nil, fmt.Errorf(`failed to parse token #%d: %w`, len(tokens)+1, err) } tokens = append(tokens, tok) } return tokens, nil } // isSpace reports whether the character is a Unicode white space character. // We avoid dependency on the unicode package, but check validity of the implementation // in the tests. func isSpace(r rune) bool { if r <= '\u00FF' { // Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs. switch r { case ' ', '\t', '\n', '\v', '\f', '\r': return true case '\u0085', '\u00A0': return true } return false } // High-valued ones. if '\u2000' <= r && r <= '\u200a' { return true } switch r { case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': return true } return false } func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte, err error) { // Skip leading spaces. start := 0 for width := 0; start < len(data); start += width { var r rune r, width = utf8.DecodeRune(data[start:]) if !isSpace(r) { break } } // Scan until we find a comma. Keep track of consecutive whitespaces // so we remove them from the end result var ws int for width, i := 0, start; i < len(data); i += width { var r rune r, width = utf8.DecodeRune(data[i:]) switch { case isSpace(r): ws++ case r == ',': return i + width, data[start : i-ws], nil default: ws = 0 } } // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. if atEOF && len(data) > start { return len(data), data[start : len(data)-ws], nil } // Request more data. return start, nil, nil } // ParseRequest parses the content of `Cache-Control` header of an HTTP Request. func ParseRequest(v string) (*RequestDirective, error) { var dir RequestDirective tokens, err := ParseRequestDirectives(v) if err != nil { return nil, fmt.Errorf(`failed to parse tokens: %w`, err) } for _, token := range tokens { name := strings.ToLower(token.Name) switch name { case MaxAge: iv, err := strconv.ParseUint(token.Value, 10, 64) if err != nil { return nil, fmt.Errorf(`failed to parse max-age: %w`, err) } dir.maxAge = &iv case MaxStale: iv, err := strconv.ParseUint(token.Value, 10, 64) if err != nil { return nil, fmt.Errorf(`failed to parse max-stale: %w`, err) } dir.maxStale = &iv case MinFresh: iv, err := strconv.ParseUint(token.Value, 10, 64) if err != nil { return nil, fmt.Errorf(`failed to parse min-fresh: %w`, err) } dir.minFresh = &iv case NoCache: dir.noCache = true case NoStore: dir.noStore = true case NoTransform: dir.noTransform = true case OnlyIfCached: dir.onlyIfCached = true default: dir.extensions[token.Name] = token.Value } } return &dir, nil } // ParseResponse parses the content of `Cache-Control` header of an HTTP Response. func ParseResponse(v string) (*ResponseDirective, error) { tokens, err := ParseResponseDirectives(v) if err != nil { return nil, fmt.Errorf(`failed to parse tokens: %w`, err) } var dir ResponseDirective dir.extensions = make(map[string]string) for _, token := range tokens { name := strings.ToLower(token.Name) switch name { case MaxAge: iv, err := strconv.ParseUint(token.Value, 10, 64) if err != nil { return nil, fmt.Errorf(`failed to parse max-age: %w`, err) } dir.maxAge = &iv case NoCache: scanner := bufio.NewScanner(strings.NewReader(token.Value)) scanner.Split(scanCommaSeparatedWords) for scanner.Scan() { dir.noCache = append(dir.noCache, scanner.Text()) } case NoStore: dir.noStore = true case NoTransform: dir.noTransform = true case Public: dir.public = true case Private: scanner := bufio.NewScanner(strings.NewReader(token.Value)) scanner.Split(scanCommaSeparatedWords) for scanner.Scan() { dir.private = append(dir.private, scanner.Text()) } case ProxyRevalidate: dir.proxyRevalidate = true case SMaxAge: iv, err := strconv.ParseUint(token.Value, 10, 64) if err != nil { return nil, fmt.Errorf(`failed to parse s-maxage: %w`, err) } dir.sMaxAge = &iv default: dir.extensions[token.Name] = token.Value } } return &dir, nil } golang-github-lestrrat-go-httpcc-1.0.1/httpcc_test.go000066400000000000000000000125711464252212300226460ustar00rootroot00000000000000package httpcc_test import ( "testing" httpcc "github.com/lestrrat-go/httpcc" "github.com/stretchr/testify/assert" ) func compareRequestDirective(t *testing.T, expected map[string]interface{}, dir *httpcc.RequestDirective) bool { t.Helper() for k, v := range expected { switch k { case httpcc.MaxAge: got, ok := dir.MaxAge() if !assert.True(t, ok, `dir.MaxAge() should return true`) { return false } if !assert.Equal(t, v, got) { return false } case httpcc.NoStore: got := dir.NoStore() if !assert.Equal(t, v, got) { return false } case "extensions": got := dir.Extensions() if !assert.Equal(t, v, got) { return false } default: assert.Fail(t, `unhandled field %s`, k) return false } } return true } func compareResponseDirective(t *testing.T, expected map[string]interface{}, dir *httpcc.ResponseDirective) bool { t.Helper() for k, v := range expected { switch k { case httpcc.MaxAge: got, ok := dir.MaxAge() if !assert.True(t, ok, `dir.MaxAge() should return true`) { return false } if !assert.Equal(t, v, got) { return false } case httpcc.NoStore: got := dir.NoStore() if !assert.Equal(t, v, got) { return false } case "extensions": got := dir.Extensions() if !assert.Equal(t, v, got) { return false } default: assert.Fail(t, `unhandled field %s`, k) return false } } return true } func TestParseDirective(t *testing.T) { testcases := []struct { Source string Error bool Expected *httpcc.TokenPair IsRequest bool }{ { Source: `no-store="foo"`, Error: true, }, { Source: `s-maxage=4649`, Expected: &httpcc.TokenPair{Name: `s-maxage`, Value: `4649`}, }, { Source: `s-maxage=4649`, Expected: &httpcc.TokenPair{Name: `s-maxage`, Value: `4649`}, IsRequest: true, }, { Source: "no-store", Expected: &httpcc.TokenPair{Name: "no-store"}, }, { Source: "max-age=4649", Expected: &httpcc.TokenPair{Name: "max-age", Value: "4649"}, IsRequest: true, }, { Source: `max-age="4649"`, Error: true, IsRequest: true, }, { Source: `max-age="4649`, Error: true, IsRequest: true, }, } for _, tc := range testcases { tc := tc t.Run(tc.Source, func(t *testing.T) { var pair *httpcc.TokenPair var err error if tc.IsRequest { pair, err = httpcc.ParseRequestDirective(tc.Source) } else { pair, err = httpcc.ParseResponseDirective(tc.Source) } if tc.Error { if !assert.Error(t, err, `expected to return an error`) { return } } else { if !assert.NoError(t, err, `expected to succeed`) { return } if !assert.Equal(t, tc.Expected, pair, `expected to return pair`) { return } } }) } } func TestParseDirectives(t *testing.T) { testcases := []struct { Source string Error bool Expected []*httpcc.TokenPair IsRequest bool }{ { Source: `max-age=4649, no-store`, IsRequest: true, Expected: []*httpcc.TokenPair{ {Name: `max-age`, Value: `4649`}, {Name: `no-store`}, }, }, { Source: ` max-age=4649 , no-store `, IsRequest: true, Expected: []*httpcc.TokenPair{ {Name: `max-age`, Value: `4649`}, {Name: `no-store`}, }, }, } for _, tc := range testcases { tc := tc t.Run(tc.Source, func(t *testing.T) { var tokens []*httpcc.TokenPair var err error if tc.IsRequest { tokens, err = httpcc.ParseRequestDirectives(tc.Source) } else { tokens, err = httpcc.ParseResponseDirectives(tc.Source) } if tc.Error { if !assert.Error(t, err, `expected to return an error`) { return } } else { if !assert.NoError(t, err, `expected to succeed`) { return } if !assert.Equal(t, tc.Expected, tokens, `expected to return list of tokens`) { return } } }) } } func TestParseRequest(t *testing.T) { testcases := []struct { Source string Error bool Expected map[string]interface{} }{ { Source: `max-age=4649, no-store`, Expected: map[string]interface{}{ httpcc.MaxAge: uint64(4649), httpcc.NoStore: true, }, }, { Source: `max-age="4649"`, Error: true, }, } for _, tc := range testcases { tc := tc t.Run(tc.Source, func(t *testing.T) { dir, err := httpcc.ParseRequest(tc.Source) if tc.Error { if !assert.Error(t, err, `expected to return an error`) { return } } else { if !assert.NoError(t, err, `expected to succeed`) { return } if !compareRequestDirective(t, tc.Expected, dir) { return } } }) } } func TestParseResponse(t *testing.T) { testcases := []struct { Source string Error bool Expected map[string]interface{} }{ { Source: `max-age=4649, no-store, community="UCI"`, Expected: map[string]interface{}{ httpcc.MaxAge: uint64(4649), httpcc.NoStore: true, "extensions": map[string]string{ "community": "UCI", }, }, }, { Source: `max-age="4649"`, Error: true, }, } for _, tc := range testcases { tc := tc t.Run(tc.Source, func(t *testing.T) { dir, err := httpcc.ParseResponse(tc.Source) if tc.Error { if !assert.Error(t, err, `expected to return an error`) { return } } else { if !assert.NoError(t, err, `expected to succeed`) { return } if !compareResponseDirective(t, tc.Expected, dir) { return } } }) } }