Skip to content

Commit

Permalink
compose a http.Response in h2quic client
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Jan 14, 2017
1 parent 0401b12 commit 1413579
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 13 deletions.
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ coverage:
round: nearest
ignore:
- ackhandler/packet_linkedlist.go
- h2quic/response.go
- utils/byteinterval_linkedlist.go
- utils/packetinterval_linkedlist.go
status:
Expand Down
32 changes: 25 additions & 7 deletions h2quic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ func (c *Client) handleHeaderStream() {
break
}

rsp := &http.Response{}
// TODO: fill in the right values
rsp, err := responseFromHeaders(mhframe)
if err != nil {
c.headerErr = qerr.Error(qerr.InternalError, err.Error())
}
headerChan <- rsp
}

Expand Down Expand Up @@ -157,7 +159,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {

hdrChan := make(chan *http.Response)
c.responses[dataStreamID] = hdrChan
_, err := c.client.OpenStream(dataStreamID)
dataStream, err := c.client.OpenStream(dataStreamID)
if err != nil {
return nil, err
}
Expand All @@ -167,20 +169,36 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
}
c.mutex.Unlock()

var rsp *http.Response
var res *http.Response
select {
case rsp = <-hdrChan:
case res = <-hdrChan:
c.mutex.Lock()
delete(c.responses, dataStreamID)
c.mutex.Unlock()
}

// if an error occured on the header stream
if rsp == nil {
if res == nil {
return nil, c.headerErr
}

return rsp, nil
// TODO: correctly set this variable
var streamEnded bool
isHead := (req.Method == "HEAD")

res = setLength(res, isHead, streamEnded)
utils.Debugf("%#v", res)

if streamEnded || isHead {
res.Body = noBody
} else {
res.Body = dataStream
}

res.Request = req
// TODO: correctly handle gzipped responses

return res, nil
}

// copied from net/transport.go
Expand Down
20 changes: 14 additions & 6 deletions h2quic/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ var _ = Describe("Client", func() {
Eventually(func() bool { return doReturned }).Should(BeTrue())
Expect(doErr).ToNot(HaveOccurred())
Expect(doRsp).To(Equal(rsp))
Expect(doRsp.Body).ToNot(BeNil())
Expect(doRsp.ContentLength).To(BeEquivalentTo(-1))
Expect(doRsp.Request).To(Equal(req))
close(done)
})

Expand Down Expand Up @@ -162,16 +165,21 @@ var _ = Describe("Client", func() {
client.responses[23] = make(chan *http.Response)
})

It("reads a response", func() {
headerStream.dataToRead.Write([]byte{
0x0, 0x0, 0x11, 0x1, 0x5, 0x0, 0x0, 0x0, 23,
// Taken from https://http2.github.io/http2-spec/compression.html#request.examples.with.huffman.coding
0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff,
})
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() {
Expand Down
111 changes: 111 additions & 0 deletions h2quic/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package h2quic

import (
"bytes"
"errors"
"io"
"io/ioutil"
"net/http"
"net/textproto"
"strconv"
"strings"

"golang.org/x/net/http2"
)

// copied from net/http2/transport.go

var errResponseHeaderListSize = errors.New("http2: response header list larger than advertised limit")
var noBody io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil))

// from the handleResponse function
func responseFromHeaders(f *http2.MetaHeadersFrame) (*http.Response, error) {
if f.Truncated {
return nil, errResponseHeaderListSize
}

status := f.PseudoValue("status")
if status == "" {
return nil, errors.New("missing status pseudo header")
}
statusCode, err := strconv.Atoi(status)
if err != nil {
return nil, errors.New("malformed non-numeric status pseudo header")
}

if statusCode == 100 {
// TODO: handle this

// traceGot100Continue(cs.trace)
// if cs.on100 != nil {
// cs.on100() // forces any write delay timer to fire
// }
// cs.pastHeaders = false // do it all again
// return nil, nil
}

header := make(http.Header)
res := &http.Response{
Proto: "HTTP/2.0",
ProtoMajor: 2,
Header: header,
StatusCode: statusCode,
Status: status + " " + http.StatusText(statusCode),
}
for _, hf := range f.RegularFields() {
key := http.CanonicalHeaderKey(hf.Name)
if key == "Trailer" {
t := res.Trailer
if t == nil {
t = make(http.Header)
res.Trailer = t
}
foreachHeaderElement(hf.Value, func(v string) {
t[http.CanonicalHeaderKey(v)] = nil
})
} else {
header[key] = append(header[key], hf.Value)
}
}

return res, nil
}

// continuation of the handleResponse function
func setLength(res *http.Response, isHead, streamEnded bool) *http.Response {
if !streamEnded || isHead {
res.ContentLength = -1
if clens := res.Header["Content-Length"]; len(clens) == 1 {
if clen64, err := strconv.ParseInt(clens[0], 10, 64); err == nil {
res.ContentLength = clen64
} else {
// TODO: care? unlike http/1, it won't mess up our framing, so it's
// more safe smuggling-wise to ignore.
}
} else if len(clens) > 1 {
// TODO: care? unlike http/1, it won't mess up our framing, so it's
// more safe smuggling-wise to ignore.
}
}
return res
}

// copied from net/http/server.go

// foreachHeaderElement splits v according to the "#rule" construction
// in RFC 2616 section 2.1 and calls fn for each non-empty element.
func foreachHeaderElement(v string, fn func(string)) {
v = textproto.TrimString(v)
if v == "" {
return
}
if !strings.Contains(v, ",") {
fn(v)
return
}
for _, f := range strings.Split(v, ",") {
if f = textproto.TrimString(f); f != "" {
fn(f)
}
}
}

0 comments on commit 1413579

Please sign in to comment.