diff --git a/codecov.yml b/codecov.yml index 4e4e039b536..597f9d9ead8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,7 @@ coverage: round: nearest ignore: - ackhandler/packet_linkedlist.go + - h2quic/response.go - utils/byteinterval_linkedlist.go - utils/packetinterval_linkedlist.go status: diff --git a/h2quic/client.go b/h2quic/client.go index e86e7659362..f89043dca53 100644 --- a/h2quic/client.go +++ b/h2quic/client.go @@ -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 } @@ -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 } @@ -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 diff --git a/h2quic/client_test.go b/h2quic/client_test.go index 061ee21faea..2e8aeec5276 100644 --- a/h2quic/client_test.go +++ b/h2quic/client_test.go @@ -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) }) @@ -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() { diff --git a/h2quic/response.go b/h2quic/response.go new file mode 100644 index 00000000000..13efdf84930 --- /dev/null +++ b/h2quic/response.go @@ -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) + } + } +}