route/vendor/github.com/lucas-clemente/quic-go/h2quic/client_test.go

474 lines
17 KiB
Go
Raw Normal View History

2017-12-12 02:51:45 +00:00
package h2quic
import (
"bytes"
"compress/gzip"
"crypto/tls"
"errors"
"net"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
quic "github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/protocol"
"github.com/lucas-clemente/quic-go/qerr"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Client", func() {
var (
client *Client
session *mockSession
headerStream *mockStream
quicTransport *QuicRoundTripper
)
BeforeEach(func() {
quicTransport = &QuicRoundTripper{}
hostname := "quic.clemente.io:1337"
client = NewClient(quicTransport, nil, hostname)
Expect(client.hostname).To(Equal(hostname))
session = &mockSession{}
client.session = session
headerStream = &mockStream{id: 3}
client.headerStream = headerStream
client.requestWriter = newRequestWriter(headerStream)
})
It("saves the TLS config", func() {
tlsConf := &tls.Config{InsecureSkipVerify: true}
client = NewClient(&QuicRoundTripper{}, tlsConf, "")
Expect(client.config.TLSConfig).To(Equal(tlsConf))
})
It("adds the port to the hostname, if none is given", func() {
client = NewClient(quicTransport, nil, "quic.clemente.io")
Expect(client.hostname).To(Equal("quic.clemente.io:443"))
})
It("dials", func() {
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
Expect(err).ToNot(HaveOccurred())
client = NewClient(quicTransport, nil, udpConn.LocalAddr().String())
go client.Dial()
data := make([]byte, 100)
_, err = udpConn.Read(data)
hdr, err := quic.ParsePublicHeader(bytes.NewReader(data), protocol.PerspectiveClient)
Expect(err).ToNot(HaveOccurred())
Expect(hdr.VersionFlag).To(BeTrue())
Expect(hdr.ConnectionID).ToNot(BeNil())
})
It("saves the session when the ConnState callback is called", func() {
client.session = nil // unset the session set in BeforeEach
client.config.ConnState(session, quic.ConnStateForwardSecure)
Expect(client.session).To(Equal(session))
})
It("opens the header stream only after the version has been negotiated", func() {
client.headerStream = nil // unset the headerStream openend in the BeforeEach
session.streamToOpen = headerStream
Expect(client.headerStream).To(BeNil()) // header stream not yet opened
// now start the actual test
client.config.ConnState(session, quic.ConnStateVersionNegotiated)
Expect(client.headerStream).ToNot(BeNil())
Expect(client.headerStream.StreamID()).To(Equal(protocol.StreamID(3)))
})
It("errors if it can't open the header stream", func() {
testErr := errors.New("test error")
client.headerStream = nil // unset the headerStream openend in the BeforeEach
session.streamOpenErr = testErr
client.config.ConnState(session, quic.ConnStateVersionNegotiated)
Expect(session.closed).To(BeTrue())
Expect(session.closedWithError).To(MatchError(testErr))
})
It("errors if the header stream has the wrong StreamID", func() {
session.streamToOpen = &mockStream{id: 1337}
client.config.ConnState(session, quic.ConnStateVersionNegotiated)
Expect(session.closed).To(BeTrue())
Expect(session.closedWithError).To(MatchError("h2quic Client BUG: StreamID of Header Stream is not 3"))
})
It("sets the correct crypto level", func() {
Expect(client.encryptionLevel).To(Equal(protocol.EncryptionUnencrypted))
client.config.ConnState(session, quic.ConnStateSecure)
Expect(client.encryptionLevel).To(Equal(protocol.EncryptionSecure))
client.config.ConnState(session, quic.ConnStateForwardSecure)
Expect(client.encryptionLevel).To(Equal(protocol.EncryptionForwardSecure))
})
Context("Doing requests", func() {
var request *http.Request
var dataStream *mockStream
getRequest := func(data []byte) *http2.MetaHeadersFrame {
r := bytes.NewReader(data)
decoder := hpack.NewDecoder(4096, func(hf hpack.HeaderField) {})
h2framer := http2.NewFramer(nil, r)
frame, err := h2framer.ReadFrame()
Expect(err).ToNot(HaveOccurred())
mhframe := &http2.MetaHeadersFrame{HeadersFrame: frame.(*http2.HeadersFrame)}
mhframe.Fields, err = decoder.DecodeFull(mhframe.HeadersFrame.HeaderBlockFragment())
Expect(err).ToNot(HaveOccurred())
return mhframe
}
getHeaderFields := func(f *http2.MetaHeadersFrame) map[string]string {
fields := make(map[string]string)
for _, hf := range f.Fields {
fields[hf.Name] = hf.Value
}
return fields
}
BeforeEach(func() {
var err error
client.encryptionLevel = protocol.EncryptionForwardSecure
request, err = http.NewRequest("https", "https://quic.clemente.io:1337/file1.dat", nil)
Expect(err).ToNot(HaveOccurred())
dataStream = &mockStream{id: 5}
session.streamToOpen = dataStream
})
It("does a request", func(done Done) {
var doRsp *http.Response
var doErr error
var doReturned bool
go func() {
doRsp, doErr = client.Do(request)
doReturned = true
}()
Eventually(func() []byte { return headerStream.dataWritten.Bytes() }).ShouldNot(BeEmpty())
Expect(client.responses).To(HaveKey(protocol.StreamID(5)))
rsp := &http.Response{
Status: "418 I'm a teapot",
StatusCode: 418,
}
client.responses[5] <- rsp
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(doErr).ToNot(HaveOccurred())
Expect(doRsp).To(Equal(rsp))
Expect(doRsp.Body).To(Equal(dataStream))
Expect(doRsp.ContentLength).To(BeEquivalentTo(-1))
Expect(doRsp.Request).To(Equal(request))
close(done)
})
It("closes the quic client when encountering an error on the header stream", func() {
var doRsp *http.Response
var doErr error
var doReturned bool
go func() {
doRsp, doErr = client.Do(request)
doReturned = true
}()
Eventually(func() chan *http.Response {
client.mutex.RLock()
defer client.mutex.RUnlock()
return client.responses[5]
}).ShouldNot(BeNil())
headerStream.dataToRead.Write([]byte("invalid response"))
client.handleHeaderStream()
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(client.headerErr).To(MatchError(qerr.Error(qerr.HeadersStreamDataDecompressFailure, "cannot read frame")))
Expect(doErr).To(MatchError(client.headerErr))
Expect(doRsp).To(BeNil())
Expect(client.session.(*mockSession).closedWithError).To(MatchError(client.headerErr))
})
It("blocks if no stream is available", func() {
session.blockOpenStreamSync = true
var doReturned bool
go func() {
defer GinkgoRecover()
_, err := client.Do(request)
Expect(err).ToNot(HaveOccurred())
doReturned = true
}()
headerStream.dataToRead.Write([]byte("invalid response"))
go client.handleHeaderStream()
Consistently(func() bool { return doReturned }).Should(BeFalse())
})
Context("validating the address", func() {
It("refuses to do requests for the wrong host", func() {
req, err := http.NewRequest("https", "https://quic.clemente.io:1336/foobar.html", nil)
Expect(err).ToNot(HaveOccurred())
_, err = client.Do(req)
Expect(err).To(MatchError("h2quic Client BUG: Do called for the wrong client"))
})
It("refuses to do plain HTTP requests", func() {
req, err := http.NewRequest("https", "http://quic.clemente.io:1337/foobar.html", nil)
Expect(err).ToNot(HaveOccurred())
_, err = client.Do(req)
Expect(err).To(MatchError("quic http2: unsupported scheme"))
})
It("adds the port for request URLs without one", func(done Done) {
var err error
client = NewClient(quicTransport, nil, "quic.clemente.io")
req, err := http.NewRequest("https", "https://quic.clemente.io/foobar.html", nil)
Expect(err).ToNot(HaveOccurred())
var doErr error
var doReturned bool
// the client.Do will block, because the encryption level is still set to Unencrypted
go func() {
_, doErr = client.Do(req)
doReturned = true
}()
Consistently(doReturned).Should(BeFalse())
Expect(doErr).ToNot(HaveOccurred())
close(done)
})
})
It("sets the EndStream header for requests without a body", func() {
go func() { client.Do(request) }()
Eventually(func() []byte { return headerStream.dataWritten.Bytes() }).ShouldNot(BeNil())
mhf := getRequest(headerStream.dataWritten.Bytes())
Expect(mhf.HeadersFrame.StreamEnded()).To(BeTrue())
})
It("sets the EndStream header to false for requests with a body", func() {
request.Body = &mockBody{}
go func() { client.Do(request) }()
Eventually(func() []byte { return headerStream.dataWritten.Bytes() }).ShouldNot(BeNil())
mhf := getRequest(headerStream.dataWritten.Bytes())
Expect(mhf.HeadersFrame.StreamEnded()).To(BeFalse())
})
Context("requests containing a Body", func() {
var requestBody []byte
var response *http.Response
BeforeEach(func() {
requestBody = []byte("request body")
body := &mockBody{}
body.SetData(requestBody)
request.Body = body
response = &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Length": []string{"1000"}},
}
})
It("sends a request", func() {
var doRsp *http.Response
var doErr error
var doReturned bool
go func() {
doRsp, doErr = client.Do(request)
doReturned = true
}()
Eventually(func() chan *http.Response { return client.responses[5] }).ShouldNot(BeNil())
client.responses[5] <- response
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(dataStream.dataWritten.Bytes()).To(Equal(requestBody))
Expect(dataStream.closed).To(BeTrue())
Expect(request.Body.(*mockBody).closed).To(BeTrue())
Expect(doErr).ToNot(HaveOccurred())
Expect(doRsp).To(Equal(response))
})
It("returns the error that occurred when reading the body", func() {
testErr := errors.New("testErr")
request.Body.(*mockBody).readErr = testErr
var doRsp *http.Response
var doErr error
var doReturned bool
go func() {
doRsp, doErr = client.Do(request)
doReturned = true
}()
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(doErr).To(MatchError(testErr))
Expect(doRsp).To(BeNil())
Expect(request.Body.(*mockBody).closed).To(BeTrue())
})
It("returns the error that occurred when closing the body", func() {
testErr := errors.New("testErr")
request.Body.(*mockBody).closeErr = testErr
var doRsp *http.Response
var doErr error
var doReturned bool
go func() {
doRsp, doErr = client.Do(request)
doReturned = true
}()
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(doErr).To(MatchError(testErr))
Expect(doRsp).To(BeNil())
Expect(request.Body.(*mockBody).closed).To(BeTrue())
})
})
Context("gzip compression", func() {
var gzippedData []byte // a gzipped foobar
var response *http.Response
BeforeEach(func() {
var b bytes.Buffer
w := gzip.NewWriter(&b)
w.Write([]byte("foobar"))
w.Close()
gzippedData = b.Bytes()
response = &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Length": []string{"1000"}},
}
})
It("adds the gzip header to requests", func() {
var doRsp *http.Response
var doErr error
go func() { doRsp, doErr = client.Do(request) }()
Eventually(func() chan *http.Response { return client.responses[5] }).ShouldNot(BeNil())
dataStream.dataToRead.Write(gzippedData)
response.Header.Add("Content-Encoding", "gzip")
client.responses[5] <- response
Eventually(func() *http.Response { return doRsp }).ShouldNot(BeNil())
Expect(doErr).ToNot(HaveOccurred())
headers := getHeaderFields(getRequest(headerStream.dataWritten.Bytes()))
Expect(headers).To(HaveKeyWithValue("accept-encoding", "gzip"))
Expect(doRsp.ContentLength).To(BeEquivalentTo(-1))
Expect(doRsp.Header.Get("Content-Encoding")).To(BeEmpty())
Expect(doRsp.Header.Get("Content-Length")).To(BeEmpty())
data := make([]byte, 6)
doRsp.Body.Read(data)
Expect(data).To(Equal([]byte("foobar")))
})
It("doesn't add gzip if the header disable it", func() {
quicTransport.DisableCompression = true
var doErr error
go func() { _, doErr = client.Do(request) }()
Eventually(func() chan *http.Response { return client.responses[5] }).ShouldNot(BeNil())
Expect(doErr).ToNot(HaveOccurred())
Eventually(func() []byte { return headerStream.dataWritten.Bytes() }).ShouldNot(BeEmpty())
headers := getHeaderFields(getRequest(headerStream.dataWritten.Bytes()))
Expect(headers).ToNot(HaveKey("accept-encoding"))
})
It("only decompresses the response if the response contains the right content-encoding header", func() {
var doRsp *http.Response
var doErr error
go func() { doRsp, doErr = client.Do(request) }()
Eventually(func() chan *http.Response { return client.responses[5] }).ShouldNot(BeNil())
dataStream.dataToRead.Write([]byte("not gzipped"))
client.responses[5] <- response
Eventually(func() *http.Response { return doRsp }).ShouldNot(BeNil())
Expect(doErr).ToNot(HaveOccurred())
headers := getHeaderFields(getRequest(headerStream.dataWritten.Bytes()))
Expect(headers).To(HaveKeyWithValue("accept-encoding", "gzip"))
data := make([]byte, 11)
doRsp.Body.Read(data)
Expect(doRsp.ContentLength).ToNot(BeEquivalentTo(-1))
Expect(data).To(Equal([]byte("not gzipped")))
})
It("doesn't add the gzip header for requests that have the accept-enconding set", func() {
request.Header.Add("accept-encoding", "gzip")
var doRsp *http.Response
var doErr error
go func() { doRsp, doErr = client.Do(request) }()
Eventually(func() chan *http.Response { return client.responses[5] }).ShouldNot(BeNil())
dataStream.dataToRead.Write([]byte("gzipped data"))
client.responses[5] <- response
Eventually(func() *http.Response { return doRsp }).ShouldNot(BeNil())
Expect(doErr).ToNot(HaveOccurred())
headers := getHeaderFields(getRequest(headerStream.dataWritten.Bytes()))
Expect(headers).To(HaveKeyWithValue("accept-encoding", "gzip"))
data := make([]byte, 12)
doRsp.Body.Read(data)
Expect(doRsp.ContentLength).ToNot(BeEquivalentTo(-1))
Expect(data).To(Equal([]byte("gzipped data")))
})
})
Context("handling the header stream", func() {
var h2framer *http2.Framer
BeforeEach(func() {
h2framer = http2.NewFramer(&headerStream.dataToRead, nil)
client.responses[23] = make(chan *http.Response)
})
It("reads header values from a response", func() {
// Taken from https://http2.github.io/http2-spec/compression.html#request.examples.with.huffman.coding
data := []byte{0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x31, 0x20, 0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d}
headerStream.dataToRead.Write([]byte{0x0, 0x0, byte(len(data)), 0x1, 0x5, 0x0, 0x0, 0x0, 23})
headerStream.dataToRead.Write(data)
go client.handleHeaderStream()
var rsp *http.Response
Eventually(client.responses[23]).Should(Receive(&rsp))
Expect(rsp).ToNot(BeNil())
Expect(rsp.Proto).To(Equal("HTTP/2.0"))
Expect(rsp.ProtoMajor).To(BeEquivalentTo(2))
Expect(rsp.StatusCode).To(BeEquivalentTo(302))
Expect(rsp.Status).To(Equal("302 Found"))
Expect(rsp.Header).To(HaveKeyWithValue("Location", []string{"https://www.example.com"}))
Expect(rsp.Header).To(HaveKeyWithValue("Cache-Control", []string{"private"}))
})
It("errors if the H2 frame is not a HeadersFrame", func() {
h2framer.WritePing(true, [8]byte{0, 0, 0, 0, 0, 0, 0, 0})
var handlerReturned bool
go func() {
client.handleHeaderStream()
handlerReturned = true
}()
var rsp *http.Response
Eventually(client.responses[23]).Should(Receive(&rsp))
Expect(rsp).To(BeNil())
Expect(client.headerErr).To(MatchError(qerr.Error(qerr.InvalidHeadersStreamData, "not a headers frame")))
Eventually(func() bool { return handlerReturned }).Should(BeTrue())
})
It("errors if it can't read the HPACK encoded header fields", func() {
h2framer.WriteHeaders(http2.HeadersFrameParam{
StreamID: 23,
EndHeaders: true,
BlockFragment: []byte("invalid HPACK data"),
})
var handlerReturned bool
go func() {
client.handleHeaderStream()
handlerReturned = true
}()
var rsp *http.Response
Eventually(client.responses[23]).Should(Receive(&rsp))
Expect(rsp).To(BeNil())
Expect(client.headerErr).To(MatchError(qerr.Error(qerr.InvalidHeadersStreamData, "cannot read header fields")))
Eventually(func() bool { return handlerReturned }).Should(BeTrue())
})
})
})
})