Update miekg/dns to v1.0.5
This commit is contained in:
parent
e210fc537e
commit
b71e04c64e
|
@ -127,14 +127,8 @@
|
|||
[[projects]]
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
revision = "5364553f1ee9cddc7ac8b62dce148309c386695b"
|
||||
version = "v1.0.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pquerna/cachecontrol"
|
||||
packages = ["cacheobject"]
|
||||
revision = "525d0eb5f91d30e3b1548de401b7ef9ea6898520"
|
||||
revision = "83c435cc65d2862736428b9b4d07d0ab10ad3e4d"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -148,7 +142,7 @@
|
|||
"poly1305",
|
||||
"salsa20/salsa"
|
||||
]
|
||||
revision = "88942b9c40a4c9d203b82b3731787b672d6e809b"
|
||||
revision = "12892e8c234f4fe6f6803f052061de9057903bb2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -160,7 +154,7 @@
|
|||
"ipv4",
|
||||
"ipv6"
|
||||
]
|
||||
revision = "6078986fec03a1dcc236c34816c71b0e05018fda"
|
||||
revision = "b68f30494add4df6bd8ef5e82803f308e7f7c59c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -172,7 +166,7 @@
|
|||
"windows/svc/eventlog",
|
||||
"windows/svc/mgr"
|
||||
]
|
||||
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
|
||||
revision = "378d26f46672a356c46195c28f61bdb4c0a781dd"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
@ -183,6 +177,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "5305be9b7e7f5f7d400b08b3b175eda810ccb13aa04049ff7efd58c13c04a809"
|
||||
inputs-digest = "b42eaffe36e3d325de06319d842b067e1c2b9f197089422d6a2df2a5947e3c4c"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -21,8 +21,6 @@ type Conn struct {
|
|||
net.Conn // a net.Conn holding the connection
|
||||
UDPSize uint16 // minimum receive buffer for UDP messages
|
||||
TsigSecret map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
|
||||
rtt time.Duration
|
||||
t time.Time
|
||||
tsigRequestMAC string
|
||||
}
|
||||
|
||||
|
@ -177,8 +175,9 @@ func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err erro
|
|||
}
|
||||
|
||||
co.TsigSecret = c.TsigSecret
|
||||
t := time.Now()
|
||||
// write with the appropriate write timeout
|
||||
co.SetWriteDeadline(time.Now().Add(c.getTimeoutForRequest(c.writeTimeout())))
|
||||
co.SetWriteDeadline(t.Add(c.getTimeoutForRequest(c.writeTimeout())))
|
||||
if err = co.WriteMsg(m); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -188,7 +187,8 @@ func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err erro
|
|||
if err == nil && r.Id != m.Id {
|
||||
err = ErrId
|
||||
}
|
||||
return r, co.rtt, err
|
||||
rtt = time.Since(t)
|
||||
return r, rtt, err
|
||||
}
|
||||
|
||||
// ReadMsg reads a message from the connection co.
|
||||
|
@ -240,7 +240,6 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
|||
}
|
||||
p = make([]byte, l)
|
||||
n, err = tcpRead(r, p)
|
||||
co.rtt = time.Since(co.t)
|
||||
default:
|
||||
if co.UDPSize > MinMsgSize {
|
||||
p = make([]byte, co.UDPSize)
|
||||
|
@ -248,7 +247,6 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
|||
p = make([]byte, MinMsgSize)
|
||||
}
|
||||
n, err = co.Read(p)
|
||||
co.rtt = time.Since(co.t)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -361,7 +359,6 @@ func (co *Conn) WriteMsg(m *Msg) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
co.t = time.Now()
|
||||
if _, err = co.Write(out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ func ClientConfigFromReader(resolvconf io.Reader) (*ClientConfig, error) {
|
|||
n = 1
|
||||
}
|
||||
c.Timeout = n
|
||||
case len(s) >= 8 && s[:9] == "attempts:":
|
||||
case len(s) >= 9 && s[:9] == "attempts:":
|
||||
n, _ := strconv.Atoi(s[9:])
|
||||
if n < 1 {
|
||||
n = 1
|
||||
|
|
|
@ -59,7 +59,35 @@ func TestNdots(t *testing.T) {
|
|||
t.Errorf("Ndots not properly parsed: (Expected: %d / Was: %d)", ndotsVariants[data], cc.Ndots)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConfigFromReaderAttempts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data string
|
||||
expected int
|
||||
}{
|
||||
{data: "options attempts:0", expected: 1},
|
||||
{data: "options attempts:1", expected: 1},
|
||||
{data: "options attempts:15", expected: 15},
|
||||
{data: "options attempts:16", expected: 16},
|
||||
{data: "options attempts:-1", expected: 1},
|
||||
{data: "options attempt:", expected: 2},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(strings.Replace(test.data, ":", " ", -1), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc, err := ClientConfigFromReader(strings.NewReader(test.data))
|
||||
if err != nil {
|
||||
t.Errorf("error parsing resolv.conf: %v", err)
|
||||
}
|
||||
if cc.Attempts != test.expected {
|
||||
t.Errorf("A attempts not properly parsed: (Expected: %d / Was: %d)", test.expected, cc.Attempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromFile(t *testing.T) {
|
||||
|
|
|
@ -142,152 +142,6 @@ func TestPackNAPTR(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCompressLength(t *testing.T) {
|
||||
m := new(Msg)
|
||||
m.SetQuestion("miek.nl", TypeMX)
|
||||
ul := m.Len()
|
||||
m.Compress = true
|
||||
if ul != m.Len() {
|
||||
t.Fatalf("should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
// Does the predicted length match final packed length?
|
||||
func TestMsgCompressLength(t *testing.T) {
|
||||
makeMsg := func(question string, ans, ns, e []RR) *Msg {
|
||||
msg := new(Msg)
|
||||
msg.SetQuestion(Fqdn(question), TypeANY)
|
||||
msg.Answer = append(msg.Answer, ans...)
|
||||
msg.Ns = append(msg.Ns, ns...)
|
||||
msg.Extra = append(msg.Extra, e...)
|
||||
msg.Compress = true
|
||||
return msg
|
||||
}
|
||||
|
||||
name1 := "12345678901234567890123456789012345.12345678.123."
|
||||
rrA := testRR(name1 + " 3600 IN A 192.0.2.1")
|
||||
rrMx := testRR(name1 + " 3600 IN MX 10 " + name1)
|
||||
tests := []*Msg{
|
||||
makeMsg(name1, []RR{rrA}, nil, nil),
|
||||
makeMsg(name1, []RR{rrMx, rrMx}, nil, nil)}
|
||||
|
||||
for _, msg := range tests {
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted < len(buf) {
|
||||
t.Errorf("predicted compressed length is wrong: predicted %s (len=%d) %d, actual %d",
|
||||
msg.Question[0].Name, len(msg.Answer), predicted, len(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLength(t *testing.T) {
|
||||
makeMsg := func(question string, ans, ns, e []RR) *Msg {
|
||||
msg := new(Msg)
|
||||
msg.SetQuestion(Fqdn(question), TypeANY)
|
||||
msg.Answer = append(msg.Answer, ans...)
|
||||
msg.Ns = append(msg.Ns, ns...)
|
||||
msg.Extra = append(msg.Extra, e...)
|
||||
return msg
|
||||
}
|
||||
|
||||
name1 := "12345678901234567890123456789012345.12345678.123."
|
||||
rrA := testRR(name1 + " 3600 IN A 192.0.2.1")
|
||||
rrMx := testRR(name1 + " 3600 IN MX 10 " + name1)
|
||||
tests := []*Msg{
|
||||
makeMsg(name1, []RR{rrA}, nil, nil),
|
||||
makeMsg(name1, []RR{rrMx, rrMx}, nil, nil)}
|
||||
|
||||
for _, msg := range tests {
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted < len(buf) {
|
||||
t.Errorf("predicted length is wrong: predicted %s (len=%d), actual %d",
|
||||
msg.Question[0].Name, predicted, len(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLength2(t *testing.T) {
|
||||
// Serialized replies
|
||||
var testMessages = []string{
|
||||
// google.com. IN A?
|
||||
"064e81800001000b0004000506676f6f676c6503636f6d0000010001c00c00010001000000050004adc22986c00c00010001000000050004adc22987c00c00010001000000050004adc22988c00c00010001000000050004adc22989c00c00010001000000050004adc2298ec00c00010001000000050004adc22980c00c00010001000000050004adc22981c00c00010001000000050004adc22982c00c00010001000000050004adc22983c00c00010001000000050004adc22984c00c00010001000000050004adc22985c00c00020001000000050006036e7331c00cc00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc0d800010001000000050004d8ef200ac0ea00010001000000050004d8ef220ac0fc00010001000000050004d8ef240ac10e00010001000000050004d8ef260a0000290500000000050000",
|
||||
// amazon.com. IN A? (reply has no EDNS0 record)
|
||||
// TODO(miek): this one is off-by-one, need to find out why
|
||||
//"6de1818000010004000a000806616d617a6f6e03636f6d0000010001c00c000100010000000500044815c2d4c00c000100010000000500044815d7e8c00c00010001000000050004b02062a6c00c00010001000000050004cdfbf236c00c000200010000000500140570646e733408756c747261646e73036f726700c00c000200010000000500150570646e733508756c747261646e7304696e666f00c00c000200010000000500160570646e733608756c747261646e7302636f02756b00c00c00020001000000050014036e7331037033310664796e656374036e657400c00c00020001000000050006036e7332c0cfc00c00020001000000050006036e7333c0cfc00c00020001000000050006036e7334c0cfc00c000200010000000500110570646e733108756c747261646e73c0dac00c000200010000000500080570646e7332c127c00c000200010000000500080570646e7333c06ec0cb00010001000000050004d04e461fc0eb00010001000000050004cc0dfa1fc0fd00010001000000050004d04e471fc10f00010001000000050004cc0dfb1fc12100010001000000050004cc4a6c01c121001c000100000005001020010502f3ff00000000000000000001c13e00010001000000050004cc4a6d01c13e001c0001000000050010261000a1101400000000000000000001",
|
||||
// yahoo.com. IN A?
|
||||
"fc2d81800001000300070008057961686f6f03636f6d0000010001c00c00010001000000050004628afd6dc00c00010001000000050004628bb718c00c00010001000000050004cebe242dc00c00020001000000050006036e7336c00cc00c00020001000000050006036e7338c00cc00c00020001000000050006036e7331c00cc00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc00c00020001000000050006036e7335c00cc07b0001000100000005000444b48310c08d00010001000000050004448eff10c09f00010001000000050004cb54dd35c0b100010001000000050004628a0b9dc0c30001000100000005000477a0f77cc05700010001000000050004ca2bdfaac06900010001000000050004caa568160000290500000000050000",
|
||||
// microsoft.com. IN A?
|
||||
"f4368180000100020005000b096d6963726f736f667403636f6d0000010001c00c0001000100000005000440040b25c00c0001000100000005000441373ac9c00c0002000100000005000e036e7331046d736674036e657400c00c00020001000000050006036e7332c04fc00c00020001000000050006036e7333c04fc00c00020001000000050006036e7334c04fc00c00020001000000050006036e7335c04fc04b000100010000000500044137253ec04b001c00010000000500102a010111200500000000000000010001c0650001000100000005000440043badc065001c00010000000500102a010111200600060000000000010001c07700010001000000050004d5c7b435c077001c00010000000500102a010111202000000000000000010001c08900010001000000050004cf2e4bfec089001c00010000000500102404f800200300000000000000010001c09b000100010000000500044137e28cc09b001c00010000000500102a010111200f000100000000000100010000290500000000050000",
|
||||
// google.com. IN MX?
|
||||
"724b8180000100050004000b06676f6f676c6503636f6d00000f0001c00c000f000100000005000c000a056173706d78016cc00cc00c000f0001000000050009001404616c7431c02ac00c000f0001000000050009001e04616c7432c02ac00c000f0001000000050009002804616c7433c02ac00c000f0001000000050009003204616c7434c02ac00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc00c00020001000000050006036e7331c00cc02a00010001000000050004adc2421bc02a001c00010000000500102a00145040080c01000000000000001bc04200010001000000050004adc2461bc05700010001000000050004adc2451bc06c000100010000000500044a7d8f1bc081000100010000000500044a7d191bc0ca00010001000000050004d8ef200ac09400010001000000050004d8ef220ac0a600010001000000050004d8ef240ac0b800010001000000050004d8ef260a0000290500000000050000",
|
||||
// reddit.com. IN A?
|
||||
"12b98180000100080000000c0672656464697403636f6d0000020001c00c0002000100000005000f046175733204616b616d036e657400c00c000200010000000500070475736534c02dc00c000200010000000500070475737733c02dc00c000200010000000500070475737735c02dc00c00020001000000050008056173696131c02dc00c00020001000000050008056173696139c02dc00c00020001000000050008056e73312d31c02dc00c0002000100000005000a076e73312d313935c02dc02800010001000000050004c30a242ec04300010001000000050004451f1d39c05600010001000000050004451f3bc7c0690001000100000005000460073240c07c000100010000000500046007fb81c090000100010000000500047c283484c090001c00010000000500102a0226f0006700000000000000000064c0a400010001000000050004c16c5b01c0a4001c000100000005001026001401000200000000000000000001c0b800010001000000050004c16c5bc3c0b8001c0001000000050010260014010002000000000000000000c30000290500000000050000",
|
||||
}
|
||||
|
||||
for i, hexData := range testMessages {
|
||||
// we won't fail the decoding of the hex
|
||||
input, _ := hex.DecodeString(hexData)
|
||||
|
||||
m := new(Msg)
|
||||
m.Unpack(input)
|
||||
m.Compress = true
|
||||
lenComp := m.Len()
|
||||
b, _ := m.Pack()
|
||||
pacComp := len(b)
|
||||
m.Compress = false
|
||||
lenUnComp := m.Len()
|
||||
b, _ = m.Pack()
|
||||
pacUnComp := len(b)
|
||||
if pacComp+1 != lenComp {
|
||||
t.Errorf("msg.Len(compressed)=%d actual=%d for test %d", lenComp, pacComp, i)
|
||||
}
|
||||
if pacUnComp+1 != lenUnComp {
|
||||
t.Errorf("msg.Len(uncompressed)=%d actual=%d for test %d", lenUnComp, pacUnComp, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLengthCompressionMalformed(t *testing.T) {
|
||||
// SOA with empty hostmaster, which is illegal
|
||||
soa := &SOA{Hdr: RR_Header{Name: ".", Rrtype: TypeSOA, Class: ClassINET, Ttl: 12345},
|
||||
Ns: ".",
|
||||
Mbox: "",
|
||||
Serial: 0,
|
||||
Refresh: 28800,
|
||||
Retry: 7200,
|
||||
Expire: 604800,
|
||||
Minttl: 60}
|
||||
m := new(Msg)
|
||||
m.Compress = true
|
||||
m.Ns = []RR{soa}
|
||||
m.Len() // Should not crash.
|
||||
}
|
||||
|
||||
func TestMsgCompressLength2(t *testing.T) {
|
||||
msg := new(Msg)
|
||||
msg.Compress = true
|
||||
msg.SetQuestion(Fqdn("bliep."), TypeANY)
|
||||
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "blaat.", Rrtype: 0x21, Class: 0x1, Ttl: 0x3c}, Port: 0x4c57, Target: "foo.bar."})
|
||||
msg.Extra = append(msg.Extra, &A{Hdr: RR_Header{Name: "foo.bar.", Rrtype: 0x1, Class: 0x1, Ttl: 0x3c}, A: net.IP{0xac, 0x11, 0x0, 0x3}})
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted != len(buf) {
|
||||
t.Errorf("predicted compressed length is wrong: predicted %s (len=%d) %d, actual %d",
|
||||
msg.Question[0].Name, len(msg.Answer), predicted, len(buf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestToRFC3597(t *testing.T) {
|
||||
a := testRR("miek.nl. IN A 10.0.1.1")
|
||||
x := new(RFC3597)
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompressLength(t *testing.T) {
|
||||
m := new(Msg)
|
||||
m.SetQuestion("miek.nl", TypeMX)
|
||||
ul := m.Len()
|
||||
m.Compress = true
|
||||
if ul != m.Len() {
|
||||
t.Fatalf("should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
// Does the predicted length match final packed length?
|
||||
func TestMsgCompressLength(t *testing.T) {
|
||||
makeMsg := func(question string, ans, ns, e []RR) *Msg {
|
||||
msg := new(Msg)
|
||||
msg.SetQuestion(Fqdn(question), TypeANY)
|
||||
msg.Answer = append(msg.Answer, ans...)
|
||||
msg.Ns = append(msg.Ns, ns...)
|
||||
msg.Extra = append(msg.Extra, e...)
|
||||
msg.Compress = true
|
||||
return msg
|
||||
}
|
||||
|
||||
name1 := "12345678901234567890123456789012345.12345678.123."
|
||||
rrA := testRR(name1 + " 3600 IN A 192.0.2.1")
|
||||
rrMx := testRR(name1 + " 3600 IN MX 10 " + name1)
|
||||
tests := []*Msg{
|
||||
makeMsg(name1, []RR{rrA}, nil, nil),
|
||||
makeMsg(name1, []RR{rrMx, rrMx}, nil, nil)}
|
||||
|
||||
for _, msg := range tests {
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted < len(buf) {
|
||||
t.Errorf("predicted compressed length is wrong: predicted %s (len=%d) %d, actual %d",
|
||||
msg.Question[0].Name, len(msg.Answer), predicted, len(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLength(t *testing.T) {
|
||||
makeMsg := func(question string, ans, ns, e []RR) *Msg {
|
||||
msg := new(Msg)
|
||||
msg.SetQuestion(Fqdn(question), TypeANY)
|
||||
msg.Answer = append(msg.Answer, ans...)
|
||||
msg.Ns = append(msg.Ns, ns...)
|
||||
msg.Extra = append(msg.Extra, e...)
|
||||
return msg
|
||||
}
|
||||
|
||||
name1 := "12345678901234567890123456789012345.12345678.123."
|
||||
rrA := testRR(name1 + " 3600 IN A 192.0.2.1")
|
||||
rrMx := testRR(name1 + " 3600 IN MX 10 " + name1)
|
||||
tests := []*Msg{
|
||||
makeMsg(name1, []RR{rrA}, nil, nil),
|
||||
makeMsg(name1, []RR{rrMx, rrMx}, nil, nil)}
|
||||
|
||||
for _, msg := range tests {
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted < len(buf) {
|
||||
t.Errorf("predicted length is wrong: predicted %s (len=%d), actual %d",
|
||||
msg.Question[0].Name, predicted, len(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLength2(t *testing.T) {
|
||||
// Serialized replies
|
||||
var testMessages = []string{
|
||||
// google.com. IN A?
|
||||
"064e81800001000b0004000506676f6f676c6503636f6d0000010001c00c00010001000000050004adc22986c00c00010001000000050004adc22987c00c00010001000000050004adc22988c00c00010001000000050004adc22989c00c00010001000000050004adc2298ec00c00010001000000050004adc22980c00c00010001000000050004adc22981c00c00010001000000050004adc22982c00c00010001000000050004adc22983c00c00010001000000050004adc22984c00c00010001000000050004adc22985c00c00020001000000050006036e7331c00cc00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc0d800010001000000050004d8ef200ac0ea00010001000000050004d8ef220ac0fc00010001000000050004d8ef240ac10e00010001000000050004d8ef260a0000290500000000050000",
|
||||
// amazon.com. IN A? (reply has no EDNS0 record)
|
||||
// TODO(miek): this one is off-by-one, need to find out why
|
||||
//"6de1818000010004000a000806616d617a6f6e03636f6d0000010001c00c000100010000000500044815c2d4c00c000100010000000500044815d7e8c00c00010001000000050004b02062a6c00c00010001000000050004cdfbf236c00c000200010000000500140570646e733408756c747261646e73036f726700c00c000200010000000500150570646e733508756c747261646e7304696e666f00c00c000200010000000500160570646e733608756c747261646e7302636f02756b00c00c00020001000000050014036e7331037033310664796e656374036e657400c00c00020001000000050006036e7332c0cfc00c00020001000000050006036e7333c0cfc00c00020001000000050006036e7334c0cfc00c000200010000000500110570646e733108756c747261646e73c0dac00c000200010000000500080570646e7332c127c00c000200010000000500080570646e7333c06ec0cb00010001000000050004d04e461fc0eb00010001000000050004cc0dfa1fc0fd00010001000000050004d04e471fc10f00010001000000050004cc0dfb1fc12100010001000000050004cc4a6c01c121001c000100000005001020010502f3ff00000000000000000001c13e00010001000000050004cc4a6d01c13e001c0001000000050010261000a1101400000000000000000001",
|
||||
// yahoo.com. IN A?
|
||||
"fc2d81800001000300070008057961686f6f03636f6d0000010001c00c00010001000000050004628afd6dc00c00010001000000050004628bb718c00c00010001000000050004cebe242dc00c00020001000000050006036e7336c00cc00c00020001000000050006036e7338c00cc00c00020001000000050006036e7331c00cc00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc00c00020001000000050006036e7335c00cc07b0001000100000005000444b48310c08d00010001000000050004448eff10c09f00010001000000050004cb54dd35c0b100010001000000050004628a0b9dc0c30001000100000005000477a0f77cc05700010001000000050004ca2bdfaac06900010001000000050004caa568160000290500000000050000",
|
||||
// microsoft.com. IN A?
|
||||
"f4368180000100020005000b096d6963726f736f667403636f6d0000010001c00c0001000100000005000440040b25c00c0001000100000005000441373ac9c00c0002000100000005000e036e7331046d736674036e657400c00c00020001000000050006036e7332c04fc00c00020001000000050006036e7333c04fc00c00020001000000050006036e7334c04fc00c00020001000000050006036e7335c04fc04b000100010000000500044137253ec04b001c00010000000500102a010111200500000000000000010001c0650001000100000005000440043badc065001c00010000000500102a010111200600060000000000010001c07700010001000000050004d5c7b435c077001c00010000000500102a010111202000000000000000010001c08900010001000000050004cf2e4bfec089001c00010000000500102404f800200300000000000000010001c09b000100010000000500044137e28cc09b001c00010000000500102a010111200f000100000000000100010000290500000000050000",
|
||||
// google.com. IN MX?
|
||||
"724b8180000100050004000b06676f6f676c6503636f6d00000f0001c00c000f000100000005000c000a056173706d78016cc00cc00c000f0001000000050009001404616c7431c02ac00c000f0001000000050009001e04616c7432c02ac00c000f0001000000050009002804616c7433c02ac00c000f0001000000050009003204616c7434c02ac00c00020001000000050006036e7332c00cc00c00020001000000050006036e7333c00cc00c00020001000000050006036e7334c00cc00c00020001000000050006036e7331c00cc02a00010001000000050004adc2421bc02a001c00010000000500102a00145040080c01000000000000001bc04200010001000000050004adc2461bc05700010001000000050004adc2451bc06c000100010000000500044a7d8f1bc081000100010000000500044a7d191bc0ca00010001000000050004d8ef200ac09400010001000000050004d8ef220ac0a600010001000000050004d8ef240ac0b800010001000000050004d8ef260a0000290500000000050000",
|
||||
// reddit.com. IN A?
|
||||
"12b98180000100080000000c0672656464697403636f6d0000020001c00c0002000100000005000f046175733204616b616d036e657400c00c000200010000000500070475736534c02dc00c000200010000000500070475737733c02dc00c000200010000000500070475737735c02dc00c00020001000000050008056173696131c02dc00c00020001000000050008056173696139c02dc00c00020001000000050008056e73312d31c02dc00c0002000100000005000a076e73312d313935c02dc02800010001000000050004c30a242ec04300010001000000050004451f1d39c05600010001000000050004451f3bc7c0690001000100000005000460073240c07c000100010000000500046007fb81c090000100010000000500047c283484c090001c00010000000500102a0226f0006700000000000000000064c0a400010001000000050004c16c5b01c0a4001c000100000005001026001401000200000000000000000001c0b800010001000000050004c16c5bc3c0b8001c0001000000050010260014010002000000000000000000c30000290500000000050000",
|
||||
}
|
||||
|
||||
for i, hexData := range testMessages {
|
||||
// we won't fail the decoding of the hex
|
||||
input, _ := hex.DecodeString(hexData)
|
||||
|
||||
m := new(Msg)
|
||||
m.Unpack(input)
|
||||
m.Compress = true
|
||||
lenComp := m.Len()
|
||||
b, _ := m.Pack()
|
||||
pacComp := len(b)
|
||||
m.Compress = false
|
||||
lenUnComp := m.Len()
|
||||
b, _ = m.Pack()
|
||||
pacUnComp := len(b)
|
||||
if pacComp+1 != lenComp {
|
||||
t.Errorf("msg.Len(compressed)=%d actual=%d for test %d", lenComp, pacComp, i)
|
||||
}
|
||||
if pacUnComp+1 != lenUnComp {
|
||||
t.Errorf("msg.Len(uncompressed)=%d actual=%d for test %d", lenUnComp, pacUnComp, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgLengthCompressionMalformed(t *testing.T) {
|
||||
// SOA with empty hostmaster, which is illegal
|
||||
soa := &SOA{Hdr: RR_Header{Name: ".", Rrtype: TypeSOA, Class: ClassINET, Ttl: 12345},
|
||||
Ns: ".",
|
||||
Mbox: "",
|
||||
Serial: 0,
|
||||
Refresh: 28800,
|
||||
Retry: 7200,
|
||||
Expire: 604800,
|
||||
Minttl: 60}
|
||||
m := new(Msg)
|
||||
m.Compress = true
|
||||
m.Ns = []RR{soa}
|
||||
m.Len() // Should not crash.
|
||||
}
|
||||
|
||||
func TestMsgCompressLength2(t *testing.T) {
|
||||
msg := new(Msg)
|
||||
msg.Compress = true
|
||||
msg.SetQuestion(Fqdn("bliep."), TypeANY)
|
||||
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "blaat.", Rrtype: 0x21, Class: 0x1, Ttl: 0x3c}, Port: 0x4c57, Target: "foo.bar."})
|
||||
msg.Extra = append(msg.Extra, &A{Hdr: RR_Header{Name: "foo.bar.", Rrtype: 0x1, Class: 0x1, Ttl: 0x3c}, A: net.IP{0xac, 0x11, 0x0, 0x3}})
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted != len(buf) {
|
||||
t.Errorf("predicted compressed length is wrong: predicted %s (len=%d) %d, actual %d",
|
||||
msg.Question[0].Name, len(msg.Answer), predicted, len(buf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgCompressLengthLargeRecords(t *testing.T) {
|
||||
msg := new(Msg)
|
||||
msg.Compress = true
|
||||
msg.SetQuestion("my.service.acme.", TypeSRV)
|
||||
j := 1
|
||||
for i := 0; i < 250; i++ {
|
||||
target := fmt.Sprintf("host-redis-%d-%d.test.acme.com.node.dc1.consul.", j, i)
|
||||
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "redis.service.consul.", Class: 1, Rrtype: TypeSRV, Ttl: 0x3c}, Port: 0x4c57, Target: target})
|
||||
msg.Extra = append(msg.Extra, &CNAME{Hdr: RR_Header{Name: target, Class: 1, Rrtype: TypeCNAME, Ttl: 0x3c}, Target: fmt.Sprintf("fx.168.%d.%d.", j, i)})
|
||||
}
|
||||
predicted := msg.Len()
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if predicted != len(buf) {
|
||||
t.Fatalf("predicted compressed length is wrong: predicted %s (len=%d) %d, actual %d", msg.Question[0].Name, len(msg.Answer), predicted, len(buf))
|
||||
}
|
||||
}
|
|
@ -595,6 +595,13 @@ func UnpackRR(msg []byte, off int) (rr RR, off1 int, err error) {
|
|||
if err != nil {
|
||||
return nil, len(msg), err
|
||||
}
|
||||
|
||||
return UnpackRRWithHeader(h, msg, off)
|
||||
}
|
||||
|
||||
// UnpackRRWithHeader unpacks the record type specific payload given an existing
|
||||
// RR_Header.
|
||||
func UnpackRRWithHeader(h RR_Header, msg []byte, off int) (rr RR, off1 int, err error) {
|
||||
end := off + int(h.Rdlength)
|
||||
|
||||
if fn, known := typeToUnpack[h.Rrtype]; !known {
|
||||
|
@ -926,49 +933,65 @@ func compressedLen(dns *Msg, compress bool) int {
|
|||
l += r.len()
|
||||
compressionLenHelper(compression, r.Name)
|
||||
}
|
||||
l += compressionLenSlice(compression, dns.Answer)
|
||||
l += compressionLenSlice(compression, dns.Ns)
|
||||
l += compressionLenSlice(compression, dns.Extra)
|
||||
} else {
|
||||
for _, r := range dns.Question {
|
||||
l += compressionLenSlice(l, compression, dns.Answer)
|
||||
l += compressionLenSlice(l, compression, dns.Ns)
|
||||
l += compressionLenSlice(l, compression, dns.Extra)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
for _, r := range dns.Question {
|
||||
l += r.len()
|
||||
}
|
||||
for _, r := range dns.Answer {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
for _, r := range dns.Answer {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
}
|
||||
for _, r := range dns.Ns {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
}
|
||||
for _, r := range dns.Extra {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
}
|
||||
for _, r := range dns.Ns {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
}
|
||||
for _, r := range dns.Extra {
|
||||
if r != nil {
|
||||
l += r.len()
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func compressionLenSlice(c map[string]int, rs []RR) int {
|
||||
func compressionLenSlice(len int, c map[string]int, rs []RR) int {
|
||||
var l int
|
||||
for _, r := range rs {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
l += r.len()
|
||||
// track this length, and the global length in len, while taking compression into account for both.
|
||||
x := r.len()
|
||||
l += x
|
||||
len += x
|
||||
|
||||
k, ok := compressionLenSearch(c, r.Header().Name)
|
||||
if ok {
|
||||
l += 1 - k
|
||||
len += 1 - k
|
||||
}
|
||||
compressionLenHelper(c, r.Header().Name)
|
||||
|
||||
if len < maxCompressionOffset {
|
||||
compressionLenHelper(c, r.Header().Name)
|
||||
}
|
||||
|
||||
k, ok = compressionLenSearchType(c, r)
|
||||
if ok {
|
||||
l += 1 - k
|
||||
len += 1 - k
|
||||
}
|
||||
|
||||
if len < maxCompressionOffset {
|
||||
compressionLenHelperType(c, r)
|
||||
}
|
||||
compressionLenHelperType(c, r)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ func RunLocalUDPServer(laddr string) (*Server, string, error) {
|
|||
return server, l, err
|
||||
}
|
||||
|
||||
func RunLocalUDPServerWithFinChan(laddr string) (*Server, string, chan struct{}, error) {
|
||||
func RunLocalUDPServerWithFinChan(laddr string) (*Server, string, chan error, error) {
|
||||
pc, err := net.ListenPacket("udp", laddr)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
|
@ -66,11 +66,13 @@ func RunLocalUDPServerWithFinChan(laddr string) (*Server, string, chan struct{},
|
|||
waitLock.Lock()
|
||||
server.NotifyStartedFunc = waitLock.Unlock
|
||||
|
||||
fin := make(chan struct{}, 0)
|
||||
// fin must be buffered so the goroutine below won't block
|
||||
// forever if fin is never read from. This always happens
|
||||
// in RunLocalUDPServer and can happen in TestShutdownUDP.
|
||||
fin := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server.ActivateAndServe()
|
||||
close(fin)
|
||||
fin <- server.ActivateAndServe()
|
||||
pc.Close()
|
||||
}()
|
||||
|
||||
|
@ -100,9 +102,15 @@ func RunLocalUDPServerUnsafe(laddr string) (*Server, string, error) {
|
|||
}
|
||||
|
||||
func RunLocalTCPServer(laddr string) (*Server, string, error) {
|
||||
server, l, _, err := RunLocalTCPServerWithFinChan(laddr)
|
||||
|
||||
return server, l, err
|
||||
}
|
||||
|
||||
func RunLocalTCPServerWithFinChan(laddr string) (*Server, string, chan error, error) {
|
||||
l, err := net.Listen("tcp", laddr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
server := &Server{Listener: l, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
|
||||
|
@ -111,13 +119,17 @@ func RunLocalTCPServer(laddr string) (*Server, string, error) {
|
|||
waitLock.Lock()
|
||||
server.NotifyStartedFunc = waitLock.Unlock
|
||||
|
||||
// See the comment in RunLocalUDPServerWithFinChan as to
|
||||
// why fin must be buffered.
|
||||
fin := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server.ActivateAndServe()
|
||||
fin <- server.ActivateAndServe()
|
||||
l.Close()
|
||||
}()
|
||||
|
||||
waitLock.Lock()
|
||||
return server, l.Addr().String(), nil
|
||||
return server, l.Addr().String(), fin, nil
|
||||
}
|
||||
|
||||
func RunLocalTLSServer(laddr string, config *tls.Config) (*Server, string, error) {
|
||||
|
@ -545,13 +557,21 @@ func TestServingResponse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShutdownTCP(t *testing.T) {
|
||||
s, _, err := RunLocalTCPServer(":0")
|
||||
s, _, fin, err := RunLocalTCPServerWithFinChan(":0")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to run test server: %v", err)
|
||||
}
|
||||
err = s.Shutdown()
|
||||
if err != nil {
|
||||
t.Errorf("could not shutdown test TCP server, %v", err)
|
||||
t.Fatalf("could not shutdown test TCP server, %v", err)
|
||||
}
|
||||
select {
|
||||
case err := <-fin:
|
||||
if err != nil {
|
||||
t.Errorf("error returned from ActivateAndServe, %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("could not shutdown test TCP server. Gave up waiting")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -642,7 +662,10 @@ func TestShutdownUDP(t *testing.T) {
|
|||
t.Errorf("could not shutdown test UDP server, %v", err)
|
||||
}
|
||||
select {
|
||||
case <-fin:
|
||||
case err := <-fin:
|
||||
if err != nil {
|
||||
t.Errorf("error returned from ActivateAndServe, %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("could not shutdown test UDP server. Gave up waiting")
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package dns
|
|||
import "fmt"
|
||||
|
||||
// Version is current version of this library.
|
||||
var Version = V{1, 0, 4}
|
||||
var Version = V{1, 0, 5}
|
||||
|
||||
// V holds the version of this library.
|
||||
type V struct {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
language: go
|
||||
|
||||
install:
|
||||
- go get -d -v ./...
|
||||
- go get -u github.com/stretchr/testify/require
|
||||
|
||||
go:
|
||||
- 1.7
|
||||
- 1.8
|
||||
- tip
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -1,107 +0,0 @@
|
|||
# cachecontrol: HTTP Caching Parser and Interpretation
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/pquerna/cachecontrol?status.svg)](https://godoc.org/github.com/pquerna/cachecontrol)[![Build Status](https://travis-ci.org/pquerna/cachecontrol.svg?branch=master)](https://travis-ci.org/pquerna/cachecontrol)
|
||||
|
||||
|
||||
|
||||
`cachecontrol` implements [RFC 7234](http://tools.ietf.org/html/rfc7234) __Hypertext Transfer Protocol (HTTP/1.1): Caching__. It does this by parsing the `Cache-Control` and other headers, providing information about requests and responses -- but `cachecontrol` does not implement an actual cache backend, just the control plane to make decisions about if a particular response is cachable.
|
||||
|
||||
# Usage
|
||||
|
||||
`cachecontrol.CachableResponse` returns an array of [reasons](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Reason) why a response should not be cached and when it expires. In the case that `len(reasons) == 0`, the response is cachable according to the RFC. However, some people want non-compliant caches for various business use cases, so each reason is specifically named, so if your cache wants to cache `POST` requests, it can easily do that, but still be RFC compliant in other situations.
|
||||
|
||||
# Examples
|
||||
|
||||
## Can you cache Example.com?
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pquerna/cachecontrol"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
req, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
_, _ = ioutil.ReadAll(res.Body)
|
||||
|
||||
reasons, expires, _ := cachecontrol.CachableResponse(req, res, cachecontrol.Options{})
|
||||
|
||||
fmt.Println("Reasons to not cache: ", reasons)
|
||||
fmt.Println("Expiration: ", expires.String())
|
||||
}
|
||||
```
|
||||
|
||||
## Can I use this in a high performance caching server?
|
||||
|
||||
`cachecontrol` is divided into two packages: `cachecontrol` with a high level API, and a lower level `cacheobject` package. Use [Object](https://godoc.org/github.com/pquerna/cachecontrol/cacheobject#Object) in a high performance use case where you have previously parsed headers containing dates or would like to avoid memory allocations.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pquerna/cachecontrol/cacheobject"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
req, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
_, _ = ioutil.ReadAll(res.Body)
|
||||
|
||||
reqDir, _ := cacheobject.ParseRequestCacheControl(req.Header.Get("Cache-Control"))
|
||||
|
||||
resDir, _ := cacheobject.ParseResponseCacheControl(res.Header.Get("Cache-Control"))
|
||||
expiresHeader, _ := http.ParseTime(res.Header.Get("Expires"))
|
||||
dateHeader, _ := http.ParseTime(res.Header.Get("Date"))
|
||||
lastModifiedHeader, _ := http.ParseTime(res.Header.Get("Last-Modified"))
|
||||
|
||||
obj := cacheobject.Object{
|
||||
RespDirectives: resDir,
|
||||
RespHeaders: res.Header,
|
||||
RespStatusCode: res.StatusCode,
|
||||
RespExpiresHeader: expiresHeader,
|
||||
RespDateHeader: dateHeader,
|
||||
RespLastModifiedHeader: lastModifiedHeader,
|
||||
|
||||
ReqDirectives: reqDir,
|
||||
ReqHeaders: req.Header,
|
||||
ReqMethod: req.Method,
|
||||
|
||||
NowUTC: time.Now().UTC(),
|
||||
}
|
||||
rv := cacheobject.ObjectResults{}
|
||||
|
||||
cacheobject.CachableObject(&obj, &rv)
|
||||
cacheobject.ExpirationObject(&obj, &rv)
|
||||
|
||||
fmt.Println("Errors: ", rv.OutErr)
|
||||
fmt.Println("Reasons to not cache: ", rv.OutReasons)
|
||||
fmt.Println("Warning headers to add: ", rv.OutWarnings)
|
||||
fmt.Println("Expiration: ", rv.OutExpirationTime.String())
|
||||
}
|
||||
```
|
||||
|
||||
## Improvements, bugs, adding features, and taking cachecontrol new directions!
|
||||
|
||||
Please [open issues in Github](https://github.com/pquerna/cachecontrol/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :)
|
||||
|
||||
# Credits
|
||||
|
||||
`cachecontrol` has recieved significant contributions from:
|
||||
|
||||
* [Paul Querna](https://github.com/pquerna)
|
||||
|
||||
## License
|
||||
|
||||
`cachecontrol` is licensed under the [Apache License, Version 2.0](./LICENSE)
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cachecontrol
|
||||
|
||||
import (
|
||||
"github.com/pquerna/cachecontrol/cacheobject"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Set to True for a prviate cache, which is not shared amoung users (eg, in a browser)
|
||||
// Set to False for a "shared" cache, which is more common in a server context.
|
||||
PrivateCache bool
|
||||
}
|
||||
|
||||
// Given an HTTP Request, the future Status Code, and an ResponseWriter,
|
||||
// determine the possible reasons a response SHOULD NOT be cached.
|
||||
func CachableResponseWriter(req *http.Request,
|
||||
statusCode int,
|
||||
resp http.ResponseWriter,
|
||||
opts Options) ([]cacheobject.Reason, time.Time, error) {
|
||||
return cacheobject.UsingRequestResponse(req, statusCode, resp.Header(), opts.PrivateCache)
|
||||
}
|
||||
|
||||
// Given an HTTP Request and Response, determine the possible reasons a response SHOULD NOT
|
||||
// be cached.
|
||||
func CachableResponse(req *http.Request,
|
||||
resp *http.Response,
|
||||
opts Options) ([]cacheobject.Reason, time.Time, error) {
|
||||
return cacheobject.UsingRequestResponse(req, resp.StatusCode, resp.Header, opts.PrivateCache)
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cachecontrol
|
||||
|
||||
import (
|
||||
"github.com/pquerna/cachecontrol/cacheobject"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func roundTrip(t *testing.T, fnc func(w http.ResponseWriter, r *http.Request)) (*http.Request, *http.Response) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(fnc))
|
||||
defer ts.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
return req, res
|
||||
}
|
||||
|
||||
func TestCachableResponsePublic(t *testing.T) {
|
||||
req, res := roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public")
|
||||
w.Header().Set("Last-Modified",
|
||||
time.Now().UTC().Add(time.Duration(time.Hour*-5)).Format(http.TimeFormat))
|
||||
fmt.Fprintln(w, `{}`)
|
||||
})
|
||||
|
||||
opts := Options{}
|
||||
reasons, expires, err := CachableResponse(req, res, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.WithinDuration(t,
|
||||
time.Now().UTC().Add(time.Duration(float64(time.Hour)*0.5)),
|
||||
expires,
|
||||
10*time.Second)
|
||||
}
|
||||
|
||||
func TestCachableResponsePrivate(t *testing.T) {
|
||||
req, res := roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
fmt.Fprintln(w, `{}`)
|
||||
})
|
||||
|
||||
opts := Options{}
|
||||
reasons, expires, err := CachableResponse(req, res, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 1)
|
||||
require.Equal(t, reasons[0], cacheobject.ReasonResponsePrivate)
|
||||
require.Equal(t, time.Time{}, expires)
|
||||
|
||||
opts.PrivateCache = true
|
||||
reasons, expires, err = CachableResponse(req, res, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.Equal(t, time.Time{}, expires)
|
||||
}
|
||||
|
||||
func TestResponseWriter(t *testing.T) {
|
||||
var resp http.ResponseWriter
|
||||
var req *http.Request
|
||||
_, _ = roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
fmt.Fprintln(w, `{}`)
|
||||
resp = w
|
||||
req = r
|
||||
})
|
||||
|
||||
opts := Options{}
|
||||
reasons, expires, err := CachableResponseWriter(req, 200, resp, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 1)
|
||||
require.Equal(t, reasons[0], cacheobject.ReasonResponsePrivate)
|
||||
require.Equal(t, time.Time{}, expires)
|
||||
|
||||
opts.PrivateCache = true
|
||||
reasons, expires, err = CachableResponseWriter(req, 200, resp, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.Equal(t, time.Time{}, expires)
|
||||
}
|
|
@ -1,546 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO(pquerna): add extensions from here: http://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
|
||||
|
||||
var (
|
||||
ErrQuoteMismatch = errors.New("Missing closing quote")
|
||||
ErrMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `max-age`")
|
||||
ErrSMaxAgeDeltaSeconds = errors.New("Failed to parse delta-seconds in `s-maxage`")
|
||||
ErrMaxStaleDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
|
||||
ErrMinFreshDeltaSeconds = errors.New("Failed to parse delta-seconds in `min-fresh`")
|
||||
ErrNoCacheNoArgs = errors.New("Unexpected argument to `no-cache`")
|
||||
ErrNoStoreNoArgs = errors.New("Unexpected argument to `no-store`")
|
||||
ErrNoTransformNoArgs = errors.New("Unexpected argument to `no-transform`")
|
||||
ErrOnlyIfCachedNoArgs = errors.New("Unexpected argument to `only-if-cached`")
|
||||
ErrMustRevalidateNoArgs = errors.New("Unexpected argument to `must-revalidate`")
|
||||
ErrPublicNoArgs = errors.New("Unexpected argument to `public`")
|
||||
ErrProxyRevalidateNoArgs = errors.New("Unexpected argument to `proxy-revalidate`")
|
||||
// Experimental
|
||||
ErrImmutableNoArgs = errors.New("Unexpected argument to `immutable`")
|
||||
ErrStaleIfErrorDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-if-error`")
|
||||
ErrStaleWhileRevalidateDeltaSeconds = errors.New("Failed to parse delta-seconds in `stale-while-revalidate`")
|
||||
)
|
||||
|
||||
func whitespace(b byte) bool {
|
||||
if b == '\t' || b == ' ' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parse(value string, cd cacheDirective) error {
|
||||
var err error = nil
|
||||
i := 0
|
||||
|
||||
for i < len(value) && err == nil {
|
||||
// eat leading whitespace or commas
|
||||
if whitespace(value[i]) || value[i] == ',' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
|
||||
for j < len(value) {
|
||||
if !isToken(value[j]) {
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
|
||||
token := strings.ToLower(value[i:j])
|
||||
tokenHasFields := hasFieldNames(token)
|
||||
/*
|
||||
println("GOT TOKEN:")
|
||||
println(" i -> ", i)
|
||||
println(" j -> ", j)
|
||||
println(" token -> ", token)
|
||||
*/
|
||||
|
||||
if j+1 < len(value) && value[j] == '=' {
|
||||
k := j + 1
|
||||
// minimum size two bytes of "", but we let httpUnquote handle it.
|
||||
if k < len(value) && value[k] == '"' {
|
||||
eaten, result := httpUnquote(value[k:])
|
||||
if eaten == -1 {
|
||||
return ErrQuoteMismatch
|
||||
}
|
||||
i = k + eaten
|
||||
|
||||
err = cd.addPair(token, result)
|
||||
} else {
|
||||
z := k
|
||||
for z < len(value) {
|
||||
if tokenHasFields {
|
||||
if whitespace(value[z]) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if whitespace(value[z]) || value[z] == ',' {
|
||||
break
|
||||
}
|
||||
}
|
||||
z++
|
||||
}
|
||||
i = z
|
||||
|
||||
result := value[k:z]
|
||||
if result != "" && result[len(result)-1] == ',' {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
|
||||
err = cd.addPair(token, result)
|
||||
}
|
||||
} else {
|
||||
if token != "," {
|
||||
err = cd.addToken(token)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeltaSeconds specifies a non-negative integer, representing
|
||||
// time in seconds: http://tools.ietf.org/html/rfc7234#section-1.2.1
|
||||
//
|
||||
// When set to -1, this means unset.
|
||||
//
|
||||
type DeltaSeconds int32
|
||||
|
||||
// Parser for delta-seconds, a uint31, more or less:
|
||||
// http://tools.ietf.org/html/rfc7234#section-1.2.1
|
||||
func parseDeltaSeconds(v string) (DeltaSeconds, error) {
|
||||
n, err := strconv.ParseUint(v, 10, 32)
|
||||
if err != nil {
|
||||
if numError, ok := err.(*strconv.NumError); ok {
|
||||
if numError.Err == strconv.ErrRange {
|
||||
return DeltaSeconds(math.MaxInt32), nil
|
||||
}
|
||||
}
|
||||
return DeltaSeconds(-1), err
|
||||
} else {
|
||||
if n > math.MaxInt32 {
|
||||
return DeltaSeconds(math.MaxInt32), nil
|
||||
} else {
|
||||
return DeltaSeconds(n), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fields present in a header.
|
||||
type FieldNames map[string]bool
|
||||
|
||||
// internal interface for shared methods of RequestCacheDirectives and ResponseCacheDirectives
|
||||
type cacheDirective interface {
|
||||
addToken(s string) error
|
||||
addPair(s string, v string) error
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Repersentation of possible request directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.1
|
||||
//
|
||||
// Note: Many fields will be `nil` in practice.
|
||||
//
|
||||
type RequestCacheDirectives struct {
|
||||
|
||||
// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.1
|
||||
//
|
||||
// The "max-age" request directive indicates that the client is
|
||||
// unwilling to accept a response whose age is greater than the
|
||||
// specified number of seconds. Unless the max-stale request directive
|
||||
// is also present, the client is not willing to accept a stale
|
||||
// response.
|
||||
MaxAge DeltaSeconds
|
||||
|
||||
// max-stale(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.2
|
||||
//
|
||||
// The "max-stale" request directive indicates that the client is
|
||||
// willing to accept a response that has exceeded its freshness
|
||||
// lifetime. If max-stale is assigned a value, then the client is
|
||||
// willing to accept a response that has exceeded its freshness lifetime
|
||||
// by no more than the specified number of seconds. If no value is
|
||||
// assigned to max-stale, then the client is willing to accept a stale
|
||||
// response of any age.
|
||||
MaxStale DeltaSeconds
|
||||
|
||||
// min-fresh(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.1.3
|
||||
//
|
||||
// The "min-fresh" request directive indicates that the client is
|
||||
// willing to accept a response whose freshness lifetime is no less than
|
||||
// its current age plus the specified time in seconds. That is, the
|
||||
// client wants a response that will still be fresh for at least the
|
||||
// specified number of seconds.
|
||||
MinFresh DeltaSeconds
|
||||
|
||||
// no-cache(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.4
|
||||
//
|
||||
// The "no-cache" request directive indicates that a cache MUST NOT use
|
||||
// a stored response to satisfy the request without successful
|
||||
// validation on the origin server.
|
||||
NoCache bool
|
||||
|
||||
// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.5
|
||||
//
|
||||
// The "no-store" request directive indicates that a cache MUST NOT
|
||||
// store any part of either this request or any response to it. This
|
||||
// directive applies to both private and shared caches.
|
||||
NoStore bool
|
||||
|
||||
// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.6
|
||||
//
|
||||
// The "no-transform" request directive indicates that an intermediary
|
||||
// (whether or not it implements a cache) MUST NOT transform the
|
||||
// payload, as defined in Section 5.7.2 of RFC7230.
|
||||
NoTransform bool
|
||||
|
||||
// only-if-cached(bool): http://tools.ietf.org/html/rfc7234#section-5.2.1.7
|
||||
//
|
||||
// The "only-if-cached" request directive indicates that the client only
|
||||
// wishes to obtain a stored response.
|
||||
OnlyIfCached bool
|
||||
|
||||
// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
|
||||
//
|
||||
// The Cache-Control header field can be extended through the use of one
|
||||
// or more cache-extension tokens, each with an optional value. A cache
|
||||
// MUST ignore unrecognized cache directives.
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
func (cd *RequestCacheDirectives) addToken(token string) error {
|
||||
var err error = nil
|
||||
|
||||
switch token {
|
||||
case "max-age":
|
||||
err = ErrMaxAgeDeltaSeconds
|
||||
case "max-stale":
|
||||
err = ErrMaxStaleDeltaSeconds
|
||||
case "min-fresh":
|
||||
err = ErrMinFreshDeltaSeconds
|
||||
case "no-cache":
|
||||
cd.NoCache = true
|
||||
case "no-store":
|
||||
cd.NoStore = true
|
||||
case "no-transform":
|
||||
cd.NoTransform = true
|
||||
case "only-if-cached":
|
||||
cd.OnlyIfCached = true
|
||||
default:
|
||||
cd.Extensions = append(cd.Extensions, token)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cd *RequestCacheDirectives) addPair(token string, v string) error {
|
||||
var err error = nil
|
||||
|
||||
switch token {
|
||||
case "max-age":
|
||||
cd.MaxAge, err = parseDeltaSeconds(v)
|
||||
if err != nil {
|
||||
err = ErrMaxAgeDeltaSeconds
|
||||
}
|
||||
case "max-stale":
|
||||
cd.MaxStale, err = parseDeltaSeconds(v)
|
||||
if err != nil {
|
||||
err = ErrMaxStaleDeltaSeconds
|
||||
}
|
||||
case "min-fresh":
|
||||
cd.MinFresh, err = parseDeltaSeconds(v)
|
||||
if err != nil {
|
||||
err = ErrMinFreshDeltaSeconds
|
||||
}
|
||||
case "no-cache":
|
||||
err = ErrNoCacheNoArgs
|
||||
case "no-store":
|
||||
err = ErrNoStoreNoArgs
|
||||
case "no-transform":
|
||||
err = ErrNoTransformNoArgs
|
||||
case "only-if-cached":
|
||||
err = ErrOnlyIfCachedNoArgs
|
||||
default:
|
||||
// TODO(pquerna): this sucks, making user re-parse
|
||||
cd.Extensions = append(cd.Extensions, token+"="+v)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Parses a Cache Control Header from a Request into a set of directives.
|
||||
func ParseRequestCacheControl(value string) (*RequestCacheDirectives, error) {
|
||||
cd := &RequestCacheDirectives{
|
||||
MaxAge: -1,
|
||||
MaxStale: -1,
|
||||
MinFresh: -1,
|
||||
}
|
||||
|
||||
err := parse(value, cd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Repersentation of possible response directives in a `Cache-Control` header: http://tools.ietf.org/html/rfc7234#section-5.2.2
|
||||
//
|
||||
// Note: Many fields will be `nil` in practice.
|
||||
//
|
||||
type ResponseCacheDirectives struct {
|
||||
|
||||
// must-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.1
|
||||
//
|
||||
// The "must-revalidate" response directive indicates that once it has
|
||||
// become stale, a cache MUST NOT use the response to satisfy subsequent
|
||||
// requests without successful validation on the origin server.
|
||||
MustRevalidate bool
|
||||
|
||||
// no-cache(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
|
||||
//
|
||||
// The "no-cache" response directive indicates that the response MUST
|
||||
// NOT be used to satisfy a subsequent request without successful
|
||||
// validation on the origin server.
|
||||
//
|
||||
// If the no-cache response directive specifies one or more field-names,
|
||||
// then a cache MAY use the response to satisfy a subsequent request,
|
||||
// subject to any other restrictions on caching. However, any header
|
||||
// fields in the response that have the field-name(s) listed MUST NOT be
|
||||
// sent in the response to a subsequent request without successful
|
||||
// revalidation with the origin server.
|
||||
NoCache FieldNames
|
||||
|
||||
// no-cache(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.2
|
||||
//
|
||||
// While the RFC defines optional field-names on a no-cache directive,
|
||||
// many applications only want to know if any no-cache directives were
|
||||
// present at all.
|
||||
NoCachePresent bool
|
||||
|
||||
// no-store(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.3
|
||||
//
|
||||
// The "no-store" request directive indicates that a cache MUST NOT
|
||||
// store any part of either this request or any response to it. This
|
||||
// directive applies to both private and shared caches.
|
||||
NoStore bool
|
||||
|
||||
// no-transform(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.4
|
||||
//
|
||||
// The "no-transform" response directive indicates that an intermediary
|
||||
// (regardless of whether it implements a cache) MUST NOT transform the
|
||||
// payload, as defined in Section 5.7.2 of RFC7230.
|
||||
NoTransform bool
|
||||
|
||||
// public(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.5
|
||||
//
|
||||
// The "public" response directive indicates that any cache MAY store
|
||||
// the response, even if the response would normally be non-cacheable or
|
||||
// cacheable only within a private cache.
|
||||
Public bool
|
||||
|
||||
// private(FieldName): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
|
||||
//
|
||||
// The "private" response directive indicates that the response message
|
||||
// is intended for a single user and MUST NOT be stored by a shared
|
||||
// cache. A private cache MAY store the response and reuse it for later
|
||||
// requests, even if the response would normally be non-cacheable.
|
||||
//
|
||||
// If the private response directive specifies one or more field-names,
|
||||
// this requirement is limited to the field-values associated with the
|
||||
// listed response header fields. That is, a shared cache MUST NOT
|
||||
// store the specified field-names(s), whereas it MAY store the
|
||||
// remainder of the response message.
|
||||
Private FieldNames
|
||||
|
||||
// private(cast-to-bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.6
|
||||
//
|
||||
// While the RFC defines optional field-names on a private directive,
|
||||
// many applications only want to know if any private directives were
|
||||
// present at all.
|
||||
PrivatePresent bool
|
||||
|
||||
// proxy-revalidate(bool): http://tools.ietf.org/html/rfc7234#section-5.2.2.7
|
||||
//
|
||||
// The "proxy-revalidate" response directive has the same meaning as the
|
||||
// must-revalidate response directive, except that it does not apply to
|
||||
// private caches.
|
||||
ProxyRevalidate bool
|
||||
|
||||
// max-age(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.8
|
||||
//
|
||||
// The "max-age" response directive indicates that the response is to be
|
||||
// considered stale after its age is greater than the specified number
|
||||
// of seconds.
|
||||
MaxAge DeltaSeconds
|
||||
|
||||
// s-maxage(delta seconds): http://tools.ietf.org/html/rfc7234#section-5.2.2.9
|
||||
//
|
||||
// The "s-maxage" response directive indicates that, in shared caches,
|
||||
// the maximum age specified by this directive overrides the maximum age
|
||||
// specified by either the max-age directive or the Expires header
|
||||
// field. The s-maxage directive also implies the semantics of the
|
||||
// proxy-revalidate response directive.
|
||||
SMaxAge DeltaSeconds
|
||||
|
||||
////
|
||||
// Experimental features
|
||||
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Extension_Cache-Control_directives
|
||||
// - https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today
|
||||
////
|
||||
|
||||
// immutable(cast-to-bool): experimental feature
|
||||
Immutable bool
|
||||
|
||||
// stale-if-error(delta seconds): experimental feature
|
||||
StaleIfError DeltaSeconds
|
||||
|
||||
// stale-while-revalidate(delta seconds): experimental feature
|
||||
StaleWhileRevalidate DeltaSeconds
|
||||
|
||||
// Extensions: http://tools.ietf.org/html/rfc7234#section-5.2.3
|
||||
//
|
||||
// The Cache-Control header field can be extended through the use of one
|
||||
// or more cache-extension tokens, each with an optional value. A cache
|
||||
// MUST ignore unrecognized cache directives.
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Parses a Cache Control Header from a Response into a set of directives.
|
||||
func ParseResponseCacheControl(value string) (*ResponseCacheDirectives, error) {
|
||||
cd := &ResponseCacheDirectives{
|
||||
MaxAge: -1,
|
||||
SMaxAge: -1,
|
||||
// Exerimantal stale timeouts
|
||||
StaleIfError: -1,
|
||||
StaleWhileRevalidate: -1,
|
||||
}
|
||||
|
||||
err := parse(value, cd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
func (cd *ResponseCacheDirectives) addToken(token string) error {
|
||||
var err error = nil
|
||||
switch token {
|
||||
case "must-revalidate":
|
||||
cd.MustRevalidate = true
|
||||
case "no-cache":
|
||||
cd.NoCachePresent = true
|
||||
case "no-store":
|
||||
cd.NoStore = true
|
||||
case "no-transform":
|
||||
cd.NoTransform = true
|
||||
case "public":
|
||||
cd.Public = true
|
||||
case "private":
|
||||
cd.PrivatePresent = true
|
||||
case "proxy-revalidate":
|
||||
cd.ProxyRevalidate = true
|
||||
case "max-age":
|
||||
err = ErrMaxAgeDeltaSeconds
|
||||
case "s-maxage":
|
||||
err = ErrSMaxAgeDeltaSeconds
|
||||
// Experimental
|
||||
case "immutable":
|
||||
cd.Immutable = true
|
||||
case "stale-if-error":
|
||||
err = ErrMaxAgeDeltaSeconds
|
||||
case "stale-while-revalidate":
|
||||
err = ErrMaxAgeDeltaSeconds
|
||||
default:
|
||||
cd.Extensions = append(cd.Extensions, token)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func hasFieldNames(token string) bool {
|
||||
switch token {
|
||||
case "no-cache":
|
||||
return true
|
||||
case "private":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cd *ResponseCacheDirectives) addPair(token string, v string) error {
|
||||
var err error = nil
|
||||
|
||||
switch token {
|
||||
case "must-revalidate":
|
||||
err = ErrMustRevalidateNoArgs
|
||||
case "no-cache":
|
||||
cd.NoCachePresent = true
|
||||
tokens := strings.Split(v, ",")
|
||||
if cd.NoCache == nil {
|
||||
cd.NoCache = make(FieldNames)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
k := http.CanonicalHeaderKey(textproto.TrimString(t))
|
||||
cd.NoCache[k] = true
|
||||
}
|
||||
case "no-store":
|
||||
err = ErrNoStoreNoArgs
|
||||
case "no-transform":
|
||||
err = ErrNoTransformNoArgs
|
||||
case "public":
|
||||
err = ErrPublicNoArgs
|
||||
case "private":
|
||||
cd.PrivatePresent = true
|
||||
tokens := strings.Split(v, ",")
|
||||
if cd.Private == nil {
|
||||
cd.Private = make(FieldNames)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
k := http.CanonicalHeaderKey(textproto.TrimString(t))
|
||||
cd.Private[k] = true
|
||||
}
|
||||
case "proxy-revalidate":
|
||||
err = ErrProxyRevalidateNoArgs
|
||||
case "max-age":
|
||||
cd.MaxAge, err = parseDeltaSeconds(v)
|
||||
case "s-maxage":
|
||||
cd.SMaxAge, err = parseDeltaSeconds(v)
|
||||
// Experimental
|
||||
case "immutable":
|
||||
err = ErrImmutableNoArgs
|
||||
case "stale-if-error":
|
||||
cd.StaleIfError, err = parseDeltaSeconds(v)
|
||||
case "stale-while-revalidate":
|
||||
cd.StaleWhileRevalidate, err = parseDeltaSeconds(v)
|
||||
default:
|
||||
// TODO(pquerna): this sucks, making user re-parse, and its technically not 'quoted' like the original,
|
||||
// but this is still easier, just a SplitN on "="
|
||||
cd.Extensions = append(cd.Extensions, token+"="+v)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,454 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaxAge(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl("")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(-1))
|
||||
|
||||
cd, err = ParseResponseCacheControl("max-age")
|
||||
require.Error(t, err)
|
||||
|
||||
cd, err = ParseResponseCacheControl("max-age=20")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(20))
|
||||
|
||||
cd, err = ParseResponseCacheControl("max-age=0")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(0))
|
||||
|
||||
cd, err = ParseResponseCacheControl("max-age=-1")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSMaxAge(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl("")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(-1))
|
||||
|
||||
cd, err = ParseResponseCacheControl("s-maxage")
|
||||
require.Error(t, err)
|
||||
|
||||
cd, err = ParseResponseCacheControl("s-maxage=20")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(20))
|
||||
|
||||
cd, err = ParseResponseCacheControl("s-maxage=0")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(0))
|
||||
|
||||
cd, err = ParseResponseCacheControl("s-maxage=-1")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestResNoCache(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl("")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(-1))
|
||||
|
||||
cd, err = ParseResponseCacheControl("no-cache")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.NoCachePresent, true)
|
||||
require.Equal(t, len(cd.NoCache), 0)
|
||||
|
||||
cd, err = ParseResponseCacheControl("no-cache=MyThing")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.NoCachePresent, true)
|
||||
require.Equal(t, len(cd.NoCache), 1)
|
||||
}
|
||||
|
||||
func TestResSpaceOnly(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(" ")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestResTabOnly(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl("\t")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestResPrivateExtensionQuoted(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`private="Set-Cookie,Request-Id" public`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.Public, true)
|
||||
require.Equal(t, cd.PrivatePresent, true)
|
||||
require.Equal(t, len(cd.Private), 2)
|
||||
require.Equal(t, len(cd.Extensions), 0)
|
||||
require.Equal(t, cd.Private["Set-Cookie"], true)
|
||||
require.Equal(t, cd.Private["Request-Id"], true)
|
||||
}
|
||||
|
||||
func TestResCommaFollowingBare(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`public, max-age=500`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.Public, true)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(500))
|
||||
require.Equal(t, cd.PrivatePresent, false)
|
||||
require.Equal(t, len(cd.Extensions), 0)
|
||||
}
|
||||
|
||||
func TestResCommaFollowingKV(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`max-age=500, public`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.Public, true)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(500))
|
||||
require.Equal(t, cd.PrivatePresent, false)
|
||||
require.Equal(t, len(cd.Extensions), 0)
|
||||
}
|
||||
|
||||
func TestResPrivateTrailingComma(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`private=Set-Cookie, public`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.Public, true)
|
||||
require.Equal(t, cd.PrivatePresent, true)
|
||||
require.Equal(t, len(cd.Private), 1)
|
||||
require.Equal(t, len(cd.Extensions), 0)
|
||||
require.Equal(t, cd.Private["Set-Cookie"], true)
|
||||
}
|
||||
|
||||
func TestResPrivateExtension(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`private=Set-Cookie,Request-Id public`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.Public, true)
|
||||
require.Equal(t, cd.PrivatePresent, true)
|
||||
require.Equal(t, len(cd.Private), 2)
|
||||
require.Equal(t, len(cd.Extensions), 0)
|
||||
require.Equal(t, cd.Private["Set-Cookie"], true)
|
||||
require.Equal(t, cd.Private["Request-Id"], true)
|
||||
}
|
||||
|
||||
func TestResMultipleNoCacheTabExtension(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl("no-cache " + "\t" + "no-cache=Mything aasdfdsfa")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.NoCachePresent, true)
|
||||
require.Equal(t, len(cd.NoCache), 1)
|
||||
require.Equal(t, len(cd.Extensions), 1)
|
||||
require.Equal(t, cd.NoCache["Mything"], true)
|
||||
}
|
||||
|
||||
func TestResExtensionsEmptyQuote(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`foo="" bar="hi"`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cd.SMaxAge, DeltaSeconds(-1))
|
||||
require.Equal(t, len(cd.Extensions), 2)
|
||||
require.Contains(t, cd.Extensions, "bar=hi")
|
||||
require.Contains(t, cd.Extensions, "foo=")
|
||||
}
|
||||
|
||||
func TestResQuoteMismatch(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`foo="`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrQuoteMismatch)
|
||||
}
|
||||
|
||||
func TestResMustRevalidateNoArgs(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`must-revalidate=234`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrMustRevalidateNoArgs)
|
||||
}
|
||||
|
||||
func TestResNoTransformNoArgs(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-transform="xxx"`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrNoTransformNoArgs)
|
||||
}
|
||||
|
||||
func TestResNoStoreNoArgs(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-store=""`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrNoStoreNoArgs)
|
||||
}
|
||||
|
||||
func TestResProxyRevalidateNoArgs(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`proxy-revalidate=23432`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrProxyRevalidateNoArgs)
|
||||
}
|
||||
|
||||
func TestResPublicNoArgs(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`public=999Vary`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrPublicNoArgs)
|
||||
}
|
||||
|
||||
func TestResMustRevalidate(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`must-revalidate`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MustRevalidate, true)
|
||||
}
|
||||
|
||||
func TestResNoTransform(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-transform`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoTransform, true)
|
||||
}
|
||||
|
||||
func TestResNoStore(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-store`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoStore, true)
|
||||
}
|
||||
|
||||
func TestResProxyRevalidate(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`proxy-revalidate`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.ProxyRevalidate, true)
|
||||
}
|
||||
|
||||
func TestResPublic(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`public`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.Public, true)
|
||||
}
|
||||
|
||||
func TestResPrivate(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`private`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Len(t, cd.Private, 0)
|
||||
require.Equal(t, cd.PrivatePresent, true)
|
||||
}
|
||||
|
||||
func TestResImmutable(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`immutable`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.Immutable, true)
|
||||
}
|
||||
|
||||
func TestResStaleIfError(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`stale-if-error=99999`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.StaleIfError, DeltaSeconds(99999))
|
||||
}
|
||||
|
||||
func TestResStaleWhileRevalidate(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`stale-while-revalidate=99999`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.StaleWhileRevalidate, DeltaSeconds(99999))
|
||||
}
|
||||
|
||||
func TestParseDeltaSecondsZero(t *testing.T) {
|
||||
ds, err := parseDeltaSeconds("0")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ds, DeltaSeconds(0))
|
||||
}
|
||||
|
||||
func TestParseDeltaSecondsLarge(t *testing.T) {
|
||||
ds, err := parseDeltaSeconds(fmt.Sprintf("%d", int64(math.MaxInt32)*2))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ds, DeltaSeconds(math.MaxInt32))
|
||||
}
|
||||
|
||||
func TestParseDeltaSecondsVeryLarge(t *testing.T) {
|
||||
ds, err := parseDeltaSeconds(fmt.Sprintf("%d", math.MaxInt64))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ds, DeltaSeconds(math.MaxInt32))
|
||||
}
|
||||
|
||||
func TestParseDeltaSecondsNegative(t *testing.T) {
|
||||
ds, err := parseDeltaSeconds("-60")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, DeltaSeconds(-1), ds)
|
||||
}
|
||||
|
||||
func TestReqNoCacheNoArgs(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`no-cache=234`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrNoCacheNoArgs)
|
||||
}
|
||||
|
||||
func TestReqNoStoreNoArgs(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`no-store=,,x`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrNoStoreNoArgs)
|
||||
}
|
||||
|
||||
func TestReqNoTransformNoArgs(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`no-transform=akx`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrNoTransformNoArgs)
|
||||
}
|
||||
|
||||
func TestReqOnlyIfCachedNoArgs(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`only-if-cached=no-store`)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, cd)
|
||||
require.Equal(t, err, ErrOnlyIfCachedNoArgs)
|
||||
}
|
||||
|
||||
func TestReqMaxAge(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`max-age=99999`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(99999))
|
||||
require.Equal(t, cd.MaxStale, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestReqMaxStale(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`max-stale=99999`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MaxStale, DeltaSeconds(99999))
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(-1))
|
||||
require.Equal(t, cd.MinFresh, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestReqMaxAgeBroken(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`max-age`)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrMaxAgeDeltaSeconds, err)
|
||||
require.Nil(t, cd)
|
||||
}
|
||||
|
||||
func TestReqMaxStaleBroken(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`max-stale`)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrMaxStaleDeltaSeconds, err)
|
||||
require.Nil(t, cd)
|
||||
}
|
||||
|
||||
func TestReqMinFresh(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh=99999`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MinFresh, DeltaSeconds(99999))
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(-1))
|
||||
require.Equal(t, cd.MaxStale, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestReqMinFreshBroken(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh`)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrMinFreshDeltaSeconds, err)
|
||||
require.Nil(t, cd)
|
||||
}
|
||||
|
||||
func TestReqMinFreshJunk(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh=a99a`)
|
||||
require.Equal(t, ErrMinFreshDeltaSeconds, err)
|
||||
require.Nil(t, cd)
|
||||
}
|
||||
|
||||
func TestReqMinFreshBadValue(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh=-1`)
|
||||
require.Equal(t, ErrMinFreshDeltaSeconds, err)
|
||||
require.Nil(t, cd)
|
||||
}
|
||||
|
||||
func TestReqExtensions(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh=99999 foobar=1 cats`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MinFresh, DeltaSeconds(99999))
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(-1))
|
||||
require.Equal(t, cd.MaxStale, DeltaSeconds(-1))
|
||||
require.Len(t, cd.Extensions, 2)
|
||||
require.Contains(t, cd.Extensions, "foobar=1")
|
||||
require.Contains(t, cd.Extensions, "cats")
|
||||
}
|
||||
|
||||
func TestReqMultiple(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`no-store no-transform`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoStore, true)
|
||||
require.Equal(t, cd.NoTransform, true)
|
||||
require.Equal(t, cd.OnlyIfCached, false)
|
||||
require.Len(t, cd.Extensions, 0)
|
||||
}
|
||||
|
||||
func TestReqMultipleComma(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`no-cache,only-if-cached`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoCache, true)
|
||||
require.Equal(t, cd.NoStore, false)
|
||||
require.Equal(t, cd.NoTransform, false)
|
||||
require.Equal(t, cd.OnlyIfCached, true)
|
||||
require.Len(t, cd.Extensions, 0)
|
||||
}
|
||||
|
||||
func TestReqLeadingComma(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`,no-cache`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Len(t, cd.Extensions, 0)
|
||||
require.Equal(t, cd.NoCache, true)
|
||||
require.Equal(t, cd.NoStore, false)
|
||||
require.Equal(t, cd.NoTransform, false)
|
||||
require.Equal(t, cd.OnlyIfCached, false)
|
||||
}
|
||||
|
||||
func TestReqMinFreshQuoted(t *testing.T) {
|
||||
cd, err := ParseRequestCacheControl(`min-fresh="99999"`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.MinFresh, DeltaSeconds(99999))
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(-1))
|
||||
require.Equal(t, cd.MaxStale, DeltaSeconds(-1))
|
||||
}
|
||||
|
||||
func TestNoSpacesIssue3(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-cache,no-store,max-age=0,must-revalidate`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoCachePresent, true)
|
||||
require.Equal(t, cd.NoStore, true)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(0))
|
||||
require.Equal(t, cd.MustRevalidate, true)
|
||||
}
|
||||
|
||||
func TestNoSpacesIssue3PrivateFields(t *testing.T) {
|
||||
cd, err := ParseResponseCacheControl(`no-cache, no-store, private=set-cookie,hello, max-age=0, must-revalidate`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cd)
|
||||
require.Equal(t, cd.NoCachePresent, true)
|
||||
require.Equal(t, cd.NoStore, true)
|
||||
require.Equal(t, cd.MaxAge, DeltaSeconds(0))
|
||||
require.Equal(t, cd.MustRevalidate, true)
|
||||
require.Equal(t, true, cd.Private["Set-Cookie"])
|
||||
require.Equal(t, true, cd.Private["Hello"])
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
// 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.
|
||||
|
||||
package cacheobject
|
||||
|
||||
// This file deals with lexical matters of HTTP
|
||||
|
||||
func isSeparator(c byte) bool {
|
||||
switch c {
|
||||
case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isCtl(c byte) bool { return (0 <= c && c <= 31) || c == 127 }
|
||||
|
||||
func isChar(c byte) bool { return 0 <= c && c <= 127 }
|
||||
|
||||
func isAnyText(c byte) bool { return !isCtl(c) }
|
||||
|
||||
func isQdText(c byte) bool { return isAnyText(c) && c != '"' }
|
||||
|
||||
func isToken(c byte) bool { return isChar(c) && !isCtl(c) && !isSeparator(c) }
|
||||
|
||||
// Valid escaped sequences are not specified in RFC 2616, so for now, we assume
|
||||
// that they coincide with the common sense ones used by GO. Malformed
|
||||
// characters should probably not be treated as errors by a robust (forgiving)
|
||||
// parser, so we replace them with the '?' character.
|
||||
func httpUnquotePair(b byte) byte {
|
||||
// skip the first byte, which should always be '\'
|
||||
switch b {
|
||||
case 'a':
|
||||
return '\a'
|
||||
case 'b':
|
||||
return '\b'
|
||||
case 'f':
|
||||
return '\f'
|
||||
case 'n':
|
||||
return '\n'
|
||||
case 'r':
|
||||
return '\r'
|
||||
case 't':
|
||||
return '\t'
|
||||
case 'v':
|
||||
return '\v'
|
||||
case '\\':
|
||||
return '\\'
|
||||
case '\'':
|
||||
return '\''
|
||||
case '"':
|
||||
return '"'
|
||||
}
|
||||
return '?'
|
||||
}
|
||||
|
||||
// raw must begin with a valid quoted string. Only the first quoted string is
|
||||
// parsed and is unquoted in result. eaten is the number of bytes parsed, or -1
|
||||
// upon failure.
|
||||
func httpUnquote(raw string) (eaten int, result string) {
|
||||
buf := make([]byte, len(raw))
|
||||
if raw[0] != '"' {
|
||||
return -1, ""
|
||||
}
|
||||
eaten = 1
|
||||
j := 0 // # of bytes written in buf
|
||||
for i := 1; i < len(raw); i++ {
|
||||
switch b := raw[i]; b {
|
||||
case '"':
|
||||
eaten++
|
||||
buf = buf[0:j]
|
||||
return i + 1, string(buf)
|
||||
case '\\':
|
||||
if len(raw) < i+2 {
|
||||
return -1, ""
|
||||
}
|
||||
buf[j] = httpUnquotePair(raw[i+1])
|
||||
eaten += 2
|
||||
j++
|
||||
i++
|
||||
default:
|
||||
if isQdText(b) {
|
||||
buf[j] = b
|
||||
} else {
|
||||
buf[j] = '?'
|
||||
}
|
||||
eaten++
|
||||
j++
|
||||
}
|
||||
}
|
||||
return -1, ""
|
||||
}
|
|
@ -1,378 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LOW LEVEL API: Repersents a potentially cachable HTTP object.
|
||||
//
|
||||
// This struct is designed to be serialized efficiently, so in a high
|
||||
// performance caching server, things like Date-Strings don't need to be
|
||||
// parsed for every use of a cached object.
|
||||
type Object struct {
|
||||
CacheIsPrivate bool
|
||||
|
||||
RespDirectives *ResponseCacheDirectives
|
||||
RespHeaders http.Header
|
||||
RespStatusCode int
|
||||
RespExpiresHeader time.Time
|
||||
RespDateHeader time.Time
|
||||
RespLastModifiedHeader time.Time
|
||||
|
||||
ReqDirectives *RequestCacheDirectives
|
||||
ReqHeaders http.Header
|
||||
ReqMethod string
|
||||
|
||||
NowUTC time.Time
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Repersents the results of examinig an Object with
|
||||
// CachableObject and ExpirationObject.
|
||||
//
|
||||
// TODO(pquerna): decide if this is a good idea or bad
|
||||
type ObjectResults struct {
|
||||
OutReasons []Reason
|
||||
OutWarnings []Warning
|
||||
OutExpirationTime time.Time
|
||||
OutErr error
|
||||
}
|
||||
|
||||
// LOW LEVEL API: Check if a object is cachable.
|
||||
func CachableObject(obj *Object, rv *ObjectResults) {
|
||||
rv.OutReasons = nil
|
||||
rv.OutWarnings = nil
|
||||
rv.OutErr = nil
|
||||
|
||||
switch obj.ReqMethod {
|
||||
case "GET":
|
||||
break
|
||||
case "HEAD":
|
||||
break
|
||||
case "POST":
|
||||
/**
|
||||
POST: http://tools.ietf.org/html/rfc7231#section-4.3.3
|
||||
|
||||
Responses to POST requests are only cacheable when they include
|
||||
explicit freshness information (see Section 4.2.1 of [RFC7234]).
|
||||
However, POST caching is not widely implemented. For cases where an
|
||||
origin server wishes the client to be able to cache the result of a
|
||||
POST in a way that can be reused by a later GET, the origin server
|
||||
MAY send a 200 (OK) response containing the result and a
|
||||
Content-Location header field that has the same value as the POST's
|
||||
effective request URI (Section 3.1.4.2).
|
||||
*/
|
||||
if !hasFreshness(obj.ReqDirectives, obj.RespDirectives, obj.RespHeaders, obj.RespExpiresHeader, obj.CacheIsPrivate) {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPOST)
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPUT)
|
||||
|
||||
case "DELETE":
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodDELETE)
|
||||
|
||||
case "CONNECT":
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodCONNECT)
|
||||
|
||||
case "OPTIONS":
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodOPTIONS)
|
||||
|
||||
case "TRACE":
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodTRACE)
|
||||
|
||||
// HTTP Extension Methods: http://www.iana.org/assignments/http-methods/http-methods.xhtml
|
||||
//
|
||||
// To my knowledge, none of them are cachable. Please open a ticket if this is not the case!
|
||||
//
|
||||
default:
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodUnkown)
|
||||
}
|
||||
|
||||
if obj.ReqDirectives.NoStore {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestNoStore)
|
||||
}
|
||||
|
||||
// Storing Responses to Authenticated Requests: http://tools.ietf.org/html/rfc7234#section-3.2
|
||||
authz := obj.ReqHeaders.Get("Authorization")
|
||||
if authz != "" {
|
||||
if obj.RespDirectives.MustRevalidate ||
|
||||
obj.RespDirectives.Public ||
|
||||
obj.RespDirectives.SMaxAge != -1 {
|
||||
// Expires of some kind present, this is potentially OK.
|
||||
} else {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonRequestAuthorizationHeader)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.RespDirectives.PrivatePresent && !obj.CacheIsPrivate {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonResponsePrivate)
|
||||
}
|
||||
|
||||
if obj.RespDirectives.NoStore {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonResponseNoStore)
|
||||
}
|
||||
|
||||
/*
|
||||
the response either:
|
||||
|
||||
* contains an Expires header field (see Section 5.3), or
|
||||
|
||||
* contains a max-age response directive (see Section 5.2.2.8), or
|
||||
|
||||
* contains a s-maxage response directive (see Section 5.2.2.9)
|
||||
and the cache is shared, or
|
||||
|
||||
* contains a Cache Control Extension (see Section 5.2.3) that
|
||||
allows it to be cached, or
|
||||
|
||||
* has a status code that is defined as cacheable by default (see
|
||||
Section 4.2.2), or
|
||||
|
||||
* contains a public response directive (see Section 5.2.2.5).
|
||||
*/
|
||||
|
||||
expires := obj.RespHeaders.Get("Expires") != ""
|
||||
statusCachable := cachableStatusCode(obj.RespStatusCode)
|
||||
|
||||
if expires ||
|
||||
obj.RespDirectives.MaxAge != -1 ||
|
||||
(obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate) ||
|
||||
statusCachable ||
|
||||
obj.RespDirectives.Public {
|
||||
/* cachable by default, at least one of the above conditions was true */
|
||||
} else {
|
||||
rv.OutReasons = append(rv.OutReasons, ReasonResponseUncachableByDefault)
|
||||
}
|
||||
}
|
||||
|
||||
var twentyFourHours = time.Duration(24 * time.Hour)
|
||||
|
||||
const debug = false
|
||||
|
||||
// LOW LEVEL API: Update an objects expiration time.
|
||||
func ExpirationObject(obj *Object, rv *ObjectResults) {
|
||||
/**
|
||||
* Okay, lets calculate Freshness/Expiration now. woo:
|
||||
* http://tools.ietf.org/html/rfc7234#section-4.2
|
||||
*/
|
||||
|
||||
/*
|
||||
o If the cache is shared and the s-maxage response directive
|
||||
(Section 5.2.2.9) is present, use its value, or
|
||||
|
||||
o If the max-age response directive (Section 5.2.2.8) is present,
|
||||
use its value, or
|
||||
|
||||
o If the Expires response header field (Section 5.3) is present, use
|
||||
its value minus the value of the Date response header field, or
|
||||
|
||||
o Otherwise, no explicit expiration time is present in the response.
|
||||
A heuristic freshness lifetime might be applicable; see
|
||||
Section 4.2.2.
|
||||
*/
|
||||
|
||||
var expiresTime time.Time
|
||||
|
||||
if obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate {
|
||||
expiresTime = obj.NowUTC.Add(time.Second * time.Duration(obj.RespDirectives.SMaxAge))
|
||||
} else if obj.RespDirectives.MaxAge != -1 {
|
||||
expiresTime = obj.NowUTC.UTC().Add(time.Second * time.Duration(obj.RespDirectives.MaxAge))
|
||||
} else if !obj.RespExpiresHeader.IsZero() {
|
||||
serverDate := obj.RespDateHeader
|
||||
if serverDate.IsZero() {
|
||||
// common enough case when a Date: header has not yet been added to an
|
||||
// active response.
|
||||
serverDate = obj.NowUTC
|
||||
}
|
||||
expiresTime = obj.NowUTC.Add(obj.RespExpiresHeader.Sub(serverDate))
|
||||
} else if !obj.RespLastModifiedHeader.IsZero() {
|
||||
// heuristic freshness lifetime
|
||||
rv.OutWarnings = append(rv.OutWarnings, WarningHeuristicExpiration)
|
||||
|
||||
// http://httpd.apache.org/docs/2.4/mod/mod_cache.html#cachelastmodifiedfactor
|
||||
// CacheMaxExpire defaults to 24 hours
|
||||
// CacheLastModifiedFactor: is 0.1
|
||||
//
|
||||
// expiry-period = MIN(time-since-last-modified-date * factor, 24 hours)
|
||||
//
|
||||
// obj.NowUTC
|
||||
|
||||
since := obj.RespLastModifiedHeader.Sub(obj.NowUTC)
|
||||
since = time.Duration(float64(since) * -0.1)
|
||||
|
||||
if since > twentyFourHours {
|
||||
expiresTime = obj.NowUTC.Add(twentyFourHours)
|
||||
} else {
|
||||
expiresTime = obj.NowUTC.Add(since)
|
||||
}
|
||||
|
||||
if debug {
|
||||
println("Now UTC: ", obj.NowUTC.String())
|
||||
println("Last-Modified: ", obj.RespLastModifiedHeader.String())
|
||||
println("Since: ", since.String())
|
||||
println("TwentyFourHours: ", twentyFourHours.String())
|
||||
println("Expiration: ", expiresTime.String())
|
||||
}
|
||||
} else {
|
||||
// TODO(pquerna): what should the default behavoir be for expiration time?
|
||||
}
|
||||
|
||||
rv.OutExpirationTime = expiresTime
|
||||
}
|
||||
|
||||
// Evaluate cachability based on an HTTP request, and parts of the response.
|
||||
func UsingRequestResponse(req *http.Request,
|
||||
statusCode int,
|
||||
respHeaders http.Header,
|
||||
privateCache bool) ([]Reason, time.Time, error) {
|
||||
|
||||
var reqHeaders http.Header
|
||||
var reqMethod string
|
||||
|
||||
var reqDir *RequestCacheDirectives = nil
|
||||
respDir, err := ParseResponseCacheControl(respHeaders.Get("Cache-Control"))
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
reqDir, err = ParseRequestCacheControl(req.Header.Get("Cache-Control"))
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
reqHeaders = req.Header
|
||||
reqMethod = req.Method
|
||||
}
|
||||
|
||||
var expiresHeader time.Time
|
||||
var dateHeader time.Time
|
||||
var lastModifiedHeader time.Time
|
||||
|
||||
if respHeaders.Get("Expires") != "" {
|
||||
expiresHeader, err = http.ParseTime(respHeaders.Get("Expires"))
|
||||
if err != nil {
|
||||
// sometimes servers will return `Expires: 0` or `Expires: -1` to
|
||||
// indicate expired content
|
||||
expiresHeader = time.Time{}
|
||||
}
|
||||
expiresHeader = expiresHeader.UTC()
|
||||
}
|
||||
|
||||
if respHeaders.Get("Date") != "" {
|
||||
dateHeader, err = http.ParseTime(respHeaders.Get("Date"))
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
dateHeader = dateHeader.UTC()
|
||||
}
|
||||
|
||||
if respHeaders.Get("Last-Modified") != "" {
|
||||
lastModifiedHeader, err = http.ParseTime(respHeaders.Get("Last-Modified"))
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
lastModifiedHeader = lastModifiedHeader.UTC()
|
||||
}
|
||||
|
||||
obj := Object{
|
||||
CacheIsPrivate: privateCache,
|
||||
|
||||
RespDirectives: respDir,
|
||||
RespHeaders: respHeaders,
|
||||
RespStatusCode: statusCode,
|
||||
RespExpiresHeader: expiresHeader,
|
||||
RespDateHeader: dateHeader,
|
||||
RespLastModifiedHeader: lastModifiedHeader,
|
||||
|
||||
ReqDirectives: reqDir,
|
||||
ReqHeaders: reqHeaders,
|
||||
ReqMethod: reqMethod,
|
||||
|
||||
NowUTC: time.Now().UTC(),
|
||||
}
|
||||
rv := ObjectResults{}
|
||||
|
||||
CachableObject(&obj, &rv)
|
||||
if rv.OutErr != nil {
|
||||
return nil, time.Time{}, rv.OutErr
|
||||
}
|
||||
|
||||
ExpirationObject(&obj, &rv)
|
||||
if rv.OutErr != nil {
|
||||
return nil, time.Time{}, rv.OutErr
|
||||
}
|
||||
|
||||
return rv.OutReasons, rv.OutExpirationTime, nil
|
||||
}
|
||||
|
||||
// calculate if a freshness directive is present: http://tools.ietf.org/html/rfc7234#section-4.2.1
|
||||
func hasFreshness(reqDir *RequestCacheDirectives, respDir *ResponseCacheDirectives, respHeaders http.Header, respExpires time.Time, privateCache bool) bool {
|
||||
if !privateCache && respDir.SMaxAge != -1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if respDir.MaxAge != -1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !respExpires.IsZero() || respHeaders.Get("Expires") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func cachableStatusCode(statusCode int) bool {
|
||||
/*
|
||||
Responses with status codes that are defined as cacheable by default
|
||||
(e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in
|
||||
this specification) can be reused by a cache with heuristic
|
||||
expiration unless otherwise indicated by the method definition or
|
||||
explicit cache controls [RFC7234]; all other status codes are not
|
||||
cacheable by default.
|
||||
*/
|
||||
switch statusCode {
|
||||
case 200:
|
||||
return true
|
||||
case 203:
|
||||
return true
|
||||
case 204:
|
||||
return true
|
||||
case 206:
|
||||
return true
|
||||
case 300:
|
||||
return true
|
||||
case 301:
|
||||
return true
|
||||
case 404:
|
||||
return true
|
||||
case 405:
|
||||
return true
|
||||
case 410:
|
||||
return true
|
||||
case 414:
|
||||
return true
|
||||
case 501:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func roundTrip(t *testing.T, fnc func(w http.ResponseWriter, r *http.Request)) (*http.Request, *http.Response) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(fnc))
|
||||
defer ts.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
return req, res
|
||||
}
|
||||
|
||||
func TestCachableResponsePublic(t *testing.T) {
|
||||
req, res := roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public")
|
||||
w.Header().Set("Last-Modified",
|
||||
time.Now().UTC().Add(time.Duration(time.Hour*-5)).Format(http.TimeFormat))
|
||||
fmt.Fprintln(w, `{}`)
|
||||
})
|
||||
|
||||
reasons, expires, err := UsingRequestResponse(req, res.StatusCode, res.Header, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.WithinDuration(t,
|
||||
time.Now().UTC().Add(time.Duration(float64(time.Hour)*0.5)),
|
||||
expires,
|
||||
10*time.Second)
|
||||
}
|
||||
|
||||
func TestCachableResponseNoHeaders(t *testing.T) {
|
||||
req, res := roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{}`)
|
||||
})
|
||||
|
||||
reasons, expires, err := UsingRequestResponse(req, res.StatusCode, res.Header, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.True(t, expires.IsZero())
|
||||
}
|
||||
|
||||
func TestCachableResponseBadExpires(t *testing.T) {
|
||||
req, res := roundTrip(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Expires", "-1")
|
||||
fmt.Fprintln(w, `{}`)
|
||||
})
|
||||
|
||||
reasons, expires, err := UsingRequestResponse(req, res.StatusCode, res.Header, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, reasons, 0)
|
||||
require.True(t, expires.IsZero())
|
||||
}
|
|
@ -1,394 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCachableStatusCode(t *testing.T) {
|
||||
ok := []int{200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501}
|
||||
for _, v := range ok {
|
||||
require.True(t, cachableStatusCode(v), "status code should be cacheable: %d", v)
|
||||
}
|
||||
|
||||
notok := []int{201, 429, 500, 504}
|
||||
for _, v := range notok {
|
||||
require.False(t, cachableStatusCode(v), "status code should not be cachable: %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func fill(t *testing.T, now time.Time) Object {
|
||||
RespDirectives, err := ParseResponseCacheControl("")
|
||||
require.NoError(t, err)
|
||||
ReqDirectives, err := ParseRequestCacheControl("")
|
||||
require.NoError(t, err)
|
||||
|
||||
obj := Object{
|
||||
RespDirectives: RespDirectives,
|
||||
RespHeaders: http.Header{},
|
||||
RespStatusCode: 200,
|
||||
RespDateHeader: now,
|
||||
|
||||
ReqDirectives: ReqDirectives,
|
||||
ReqHeaders: http.Header{},
|
||||
ReqMethod: "GET",
|
||||
|
||||
NowUTC: now,
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func TestGETPrivate(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
RespDirectives, err := ParseResponseCacheControl("private")
|
||||
require.NoError(t, err)
|
||||
|
||||
obj.RespDirectives = RespDirectives
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonResponsePrivate)
|
||||
}
|
||||
|
||||
func TestGETPrivateWithPrivateCache(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
RespDirectives, err := ParseResponseCacheControl("private")
|
||||
require.NoError(t, err)
|
||||
|
||||
obj.CacheIsPrivate = true
|
||||
obj.RespDirectives = RespDirectives
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
}
|
||||
|
||||
func TestUncachableMethods(t *testing.T) {
|
||||
type methodPair struct {
|
||||
m string
|
||||
r Reason
|
||||
}
|
||||
|
||||
tc := []methodPair{
|
||||
{"PUT", ReasonRequestMethodPUT},
|
||||
{"DELETE", ReasonRequestMethodDELETE},
|
||||
{"CONNECT", ReasonRequestMethodCONNECT},
|
||||
{"OPTIONS", ReasonRequestMethodOPTIONS},
|
||||
{"CONNECT", ReasonRequestMethodCONNECT},
|
||||
{"TRACE", ReasonRequestMethodTRACE},
|
||||
{"MADEUP", ReasonRequestMethodUnkown},
|
||||
}
|
||||
|
||||
for _, mp := range tc {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = mp.m
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, mp.r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHEAD(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "HEAD"
|
||||
obj.RespLastModifiedHeader = now.Add(time.Hour * -1)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
require.False(t, rv.OutExpirationTime.IsZero())
|
||||
}
|
||||
|
||||
func TestHEADLongLastModified(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "HEAD"
|
||||
obj.RespLastModifiedHeader = now.Add(time.Hour * -70000)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
require.False(t, rv.OutExpirationTime.IsZero())
|
||||
require.WithinDuration(t, now.Add(twentyFourHours), rv.OutExpirationTime, time.Second*60)
|
||||
}
|
||||
|
||||
func TestNonCachablePOST(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "POST"
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestMethodPOST)
|
||||
}
|
||||
|
||||
func TestCachablePOSTExpiresHeader(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "POST"
|
||||
obj.RespExpiresHeader = now.Add(time.Hour * 1)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
}
|
||||
|
||||
func TestCachablePOSTSMax(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "POST"
|
||||
obj.RespDirectives.SMaxAge = DeltaSeconds(900)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
}
|
||||
|
||||
func TestNonCachablePOSTSMax(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "POST"
|
||||
obj.CacheIsPrivate = true
|
||||
obj.RespDirectives.SMaxAge = DeltaSeconds(900)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestMethodPOST)
|
||||
}
|
||||
|
||||
func TestCachablePOSTMax(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "POST"
|
||||
obj.RespDirectives.MaxAge = DeltaSeconds(9000)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
}
|
||||
|
||||
func TestPUTs(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "PUT"
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestMethodPUT)
|
||||
}
|
||||
|
||||
func TestPUTWithExpires(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqMethod = "PUT"
|
||||
obj.RespExpiresHeader = now.Add(time.Hour * 1)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestMethodPUT)
|
||||
}
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqHeaders.Set("Authorization", "bearer random")
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestAuthorizationHeader)
|
||||
}
|
||||
|
||||
func TestCachableAuthorization(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqHeaders.Set("Authorization", "bearer random")
|
||||
obj.RespDirectives.Public = true
|
||||
obj.RespDirectives.MaxAge = DeltaSeconds(300)
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.NoError(t, rv.OutErr)
|
||||
require.Len(t, rv.OutReasons, 0)
|
||||
}
|
||||
|
||||
func TestRespNoStore(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.RespDirectives.NoStore = true
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonResponseNoStore)
|
||||
}
|
||||
|
||||
func TestReqNoStore(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.ReqDirectives.NoStore = true
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonRequestNoStore)
|
||||
}
|
||||
|
||||
func TestResp500(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.RespStatusCode = 500
|
||||
|
||||
rv := ObjectResults{}
|
||||
CachableObject(&obj, &rv)
|
||||
require.Len(t, rv.OutReasons, 1)
|
||||
require.Contains(t, rv.OutReasons, ReasonResponseUncachableByDefault)
|
||||
}
|
||||
|
||||
func TestExpirationSMaxShared(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.RespDirectives.SMaxAge = DeltaSeconds(60)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.WithinDuration(t, now.Add(time.Second*60), rv.OutExpirationTime, time.Second*1)
|
||||
}
|
||||
|
||||
func TestExpirationSMaxPrivate(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.CacheIsPrivate = true
|
||||
obj.RespDirectives.SMaxAge = DeltaSeconds(60)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.True(t, rv.OutExpirationTime.IsZero())
|
||||
}
|
||||
|
||||
func TestExpirationMax(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
obj.RespDirectives.MaxAge = DeltaSeconds(60)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.WithinDuration(t, now.Add(time.Second*60), rv.OutExpirationTime, time.Second*1)
|
||||
}
|
||||
|
||||
func TestExpirationMaxAndSMax(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
// cache should select the SMax age since this is a shared cache.
|
||||
obj.RespDirectives.MaxAge = DeltaSeconds(60)
|
||||
obj.RespDirectives.SMaxAge = DeltaSeconds(900)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.WithinDuration(t, now.Add(time.Second*900), rv.OutExpirationTime, time.Second*1)
|
||||
}
|
||||
|
||||
func TestExpirationExpires(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
// cache should select the SMax age since this is a shared cache.
|
||||
obj.RespExpiresHeader = now.Add(time.Second * 1500)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.WithinDuration(t, now.Add(time.Second*1500), rv.OutExpirationTime, time.Second*1)
|
||||
}
|
||||
|
||||
func TestExpirationExpiresNoServerDate(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
obj := fill(t, now)
|
||||
// cache should select the SMax age since this is a shared cache.
|
||||
obj.RespDateHeader = time.Time{}
|
||||
obj.RespExpiresHeader = now.Add(time.Second * 1500)
|
||||
|
||||
rv := ObjectResults{}
|
||||
ExpirationObject(&obj, &rv)
|
||||
require.Len(t, rv.OutWarnings, 0)
|
||||
require.WithinDuration(t, now.Add(time.Second*1500), rv.OutExpirationTime, time.Second*1)
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
// Repersents a potential Reason to not cache an object.
|
||||
//
|
||||
// Applications may wish to ignore specific reasons, which will make them non-RFC
|
||||
// compliant, but this type gives them specific cases they can choose to ignore,
|
||||
// making them compliant in as many cases as they can.
|
||||
type Reason int
|
||||
|
||||
const (
|
||||
|
||||
// The request method was POST and an Expiration header was not supplied.
|
||||
ReasonRequestMethodPOST Reason = iota
|
||||
|
||||
// The request method was PUT and PUTs are not cachable.
|
||||
ReasonRequestMethodPUT
|
||||
|
||||
// The request method was DELETE and DELETEs are not cachable.
|
||||
ReasonRequestMethodDELETE
|
||||
|
||||
// The request method was CONNECT and CONNECTs are not cachable.
|
||||
ReasonRequestMethodCONNECT
|
||||
|
||||
// The request method was OPTIONS and OPTIONS are not cachable.
|
||||
ReasonRequestMethodOPTIONS
|
||||
|
||||
// The request method was TRACE and TRACEs are not cachable.
|
||||
ReasonRequestMethodTRACE
|
||||
|
||||
// The request method was not recognized by cachecontrol, and should not be cached.
|
||||
ReasonRequestMethodUnkown
|
||||
|
||||
// The request included an Cache-Control: no-store header
|
||||
ReasonRequestNoStore
|
||||
|
||||
// The request included an Authorization header without an explicit Public or Expiration time: http://tools.ietf.org/html/rfc7234#section-3.2
|
||||
ReasonRequestAuthorizationHeader
|
||||
|
||||
// The response included an Cache-Control: no-store header
|
||||
ReasonResponseNoStore
|
||||
|
||||
// The response included an Cache-Control: private header and this is not a Private cache
|
||||
ReasonResponsePrivate
|
||||
|
||||
// The response failed to meet at least one of the conditions specified in RFC 7234 section 3: http://tools.ietf.org/html/rfc7234#section-3
|
||||
ReasonResponseUncachableByDefault
|
||||
)
|
||||
|
||||
func (r Reason) String() string {
|
||||
switch r {
|
||||
case ReasonRequestMethodPOST:
|
||||
return "ReasonRequestMethodPOST"
|
||||
case ReasonRequestMethodPUT:
|
||||
return "ReasonRequestMethodPUT"
|
||||
case ReasonRequestMethodDELETE:
|
||||
return "ReasonRequestMethodDELETE"
|
||||
case ReasonRequestMethodCONNECT:
|
||||
return "ReasonRequestMethodCONNECT"
|
||||
case ReasonRequestMethodOPTIONS:
|
||||
return "ReasonRequestMethodOPTIONS"
|
||||
case ReasonRequestMethodTRACE:
|
||||
return "ReasonRequestMethodTRACE"
|
||||
case ReasonRequestMethodUnkown:
|
||||
return "ReasonRequestMethodUnkown"
|
||||
case ReasonRequestNoStore:
|
||||
return "ReasonRequestNoStore"
|
||||
case ReasonRequestAuthorizationHeader:
|
||||
return "ReasonRequestAuthorizationHeader"
|
||||
case ReasonResponseNoStore:
|
||||
return "ReasonResponseNoStore"
|
||||
case ReasonResponsePrivate:
|
||||
return "ReasonResponsePrivate"
|
||||
case ReasonResponseUncachableByDefault:
|
||||
return "ReasonResponseUncachableByDefault"
|
||||
}
|
||||
|
||||
panic(r)
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cacheobject
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Repersents an HTTP Warning: http://tools.ietf.org/html/rfc7234#section-5.5
|
||||
type Warning int
|
||||
|
||||
const (
|
||||
// Response is Stale
|
||||
// A cache SHOULD generate this whenever the sent response is stale.
|
||||
WarningResponseIsStale Warning = 110
|
||||
|
||||
// Revalidation Failed
|
||||
// A cache SHOULD generate this when sending a stale
|
||||
// response because an attempt to validate the response failed, due to an
|
||||
// inability to reach the server.
|
||||
WarningRevalidationFailed Warning = 111
|
||||
|
||||
// Disconnected Operation
|
||||
// A cache SHOULD generate this if it is intentionally disconnected from
|
||||
// the rest of the network for a period of time.
|
||||
WarningDisconnectedOperation Warning = 112
|
||||
|
||||
// Heuristic Expiration
|
||||
//
|
||||
// A cache SHOULD generate this if it heuristically chose a freshness
|
||||
// lifetime greater than 24 hours and the response's age is greater than
|
||||
// 24 hours.
|
||||
WarningHeuristicExpiration Warning = 113
|
||||
|
||||
// Miscellaneous Warning
|
||||
//
|
||||
// The warning text can include arbitrary information to be presented to
|
||||
// a human user or logged. A system receiving this warning MUST NOT
|
||||
// take any automated action, besides presenting the warning to the
|
||||
// user.
|
||||
WarningMiscellaneousWarning Warning = 199
|
||||
|
||||
// Transformation Applied
|
||||
//
|
||||
// This Warning code MUST be added by a proxy if it applies any
|
||||
// transformation to the representation, such as changing the
|
||||
// content-coding, media-type, or modifying the representation data,
|
||||
// unless this Warning code already appears in the response.
|
||||
WarningTransformationApplied Warning = 214
|
||||
|
||||
// Miscellaneous Persistent Warning
|
||||
//
|
||||
// The warning text can include arbitrary information to be presented to
|
||||
// a human user or logged. A system receiving this warning MUST NOT
|
||||
// take any automated action.
|
||||
WarningMiscellaneousPersistentWarning Warning = 299
|
||||
)
|
||||
|
||||
func (w Warning) HeaderString(agent string, date time.Time) string {
|
||||
if agent == "" {
|
||||
agent = "-"
|
||||
} else {
|
||||
// TODO(pquerna): this doesn't escape agent if it contains bad things.
|
||||
agent = `"` + agent + `"`
|
||||
}
|
||||
return fmt.Sprintf(`%d %s "%s" %s`, w, agent, w.String(), date.Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
func (w Warning) String() string {
|
||||
switch w {
|
||||
case WarningResponseIsStale:
|
||||
return "Response is Stale"
|
||||
case WarningRevalidationFailed:
|
||||
return "Revalidation Failed"
|
||||
case WarningDisconnectedOperation:
|
||||
return "Disconnected Operation"
|
||||
case WarningHeuristicExpiration:
|
||||
return "Heuristic Expiration"
|
||||
case WarningMiscellaneousWarning:
|
||||
// TODO(pquerna): ideally had a better way to override this one code.
|
||||
return "Miscellaneous Warning"
|
||||
case WarningTransformationApplied:
|
||||
return "Transformation Applied"
|
||||
case WarningMiscellaneousPersistentWarning:
|
||||
// TODO(pquerna): same as WarningMiscellaneousWarning
|
||||
return "Miscellaneous Persistent Warning"
|
||||
}
|
||||
|
||||
panic(w)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Copyright 2015 Paul Querna
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package cachecontrol implements the logic for HTTP Caching
|
||||
//
|
||||
// Deciding if an HTTP Response can be cached is often harder
|
||||
// and more bug prone than an actual cache storage backend.
|
||||
// cachecontrol provides a simple interface to determine if
|
||||
// request and response pairs are cachable as defined under
|
||||
// RFC 7234 http://tools.ietf.org/html/rfc7234
|
||||
package cachecontrol
|
Loading…
Reference in New Issue