|
|
@@ -9,24 +9,14 @@ package http2
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
- "crypto/tls"
|
|
|
"errors"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
- "io"
|
|
|
- "io/ioutil"
|
|
|
- "log"
|
|
|
- "net"
|
|
|
"net/http"
|
|
|
- "net/http/httptest"
|
|
|
- "os"
|
|
|
"os/exec"
|
|
|
- "reflect"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
- "sync/atomic"
|
|
|
"testing"
|
|
|
- "time"
|
|
|
|
|
|
"github.com/bradfitz/http2/hpack"
|
|
|
)
|
|
|
@@ -36,785 +26,6 @@ func init() {
|
|
|
flag.BoolVar(&VerboseLogs, "verboseh2", false, "Verbose HTTP/2 debug logging")
|
|
|
}
|
|
|
|
|
|
-type serverTester struct {
|
|
|
- cc net.Conn // client conn
|
|
|
- t *testing.T
|
|
|
- ts *httptest.Server
|
|
|
- fr *Framer
|
|
|
- logBuf *bytes.Buffer
|
|
|
-}
|
|
|
-
|
|
|
-func newServerTester(t *testing.T, handler http.HandlerFunc) *serverTester {
|
|
|
- logBuf := new(bytes.Buffer)
|
|
|
- ts := httptest.NewUnstartedServer(handler)
|
|
|
- ConfigureServer(ts.Config, &Server{})
|
|
|
- ts.TLS = ts.Config.TLSConfig // the httptest.Server has its own copy of this TLS config
|
|
|
- ts.Config.ErrorLog = log.New(io.MultiWriter(twriter{t: t}, logBuf), "", log.LstdFlags)
|
|
|
- ts.StartTLS()
|
|
|
-
|
|
|
- if VerboseLogs {
|
|
|
- t.Logf("Running test server at: %s", ts.URL)
|
|
|
- }
|
|
|
- cc, err := tls.Dial("tcp", ts.Listener.Addr().String(), &tls.Config{
|
|
|
- InsecureSkipVerify: true,
|
|
|
- NextProtos: []string{npnProto},
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- log.SetOutput(twriter{t})
|
|
|
- return &serverTester{
|
|
|
- t: t,
|
|
|
- ts: ts,
|
|
|
- cc: cc,
|
|
|
- fr: NewFramer(cc, cc),
|
|
|
- logBuf: logBuf,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) Close() {
|
|
|
- st.ts.Close()
|
|
|
- st.cc.Close()
|
|
|
- log.SetOutput(os.Stderr)
|
|
|
-}
|
|
|
-
|
|
|
-// greet initiates the client's HTTP/2 connection into a state where
|
|
|
-// frames may be sent.
|
|
|
-func (st *serverTester) greet() {
|
|
|
- st.writePreface()
|
|
|
- st.writeInitialSettings()
|
|
|
- st.wantSettings()
|
|
|
- st.writeSettingsAck()
|
|
|
- st.wantSettingsAck()
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) writePreface() {
|
|
|
- n, err := st.cc.Write(clientPreface)
|
|
|
- if err != nil {
|
|
|
- st.t.Fatalf("Error writing client preface: %v", err)
|
|
|
- }
|
|
|
- if n != len(clientPreface) {
|
|
|
- st.t.Fatalf("Writing client preface, wrote %d bytes; want %d", n, len(clientPreface))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) writeInitialSettings() {
|
|
|
- if err := st.fr.WriteSettings(); err != nil {
|
|
|
- st.t.Fatalf("Error writing initial SETTINGS frame from client to server: %v", err)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) writeSettingsAck() {
|
|
|
- if err := st.fr.WriteSettingsAck(); err != nil {
|
|
|
- st.t.Fatalf("Error writing ACK of server's SETTINGS: %v", err)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) writeHeaders(p HeadersFrameParam) {
|
|
|
- if err := st.fr.WriteHeaders(p); err != nil {
|
|
|
- st.t.Fatalf("Error writing HEADERS: %v", err)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// bodylessReq1 writes a HEADERS frames with StreamID 1 and EndStream and EndHeaders set.
|
|
|
-func (st *serverTester) bodylessReq1(headers ...string) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(st.t, headers...),
|
|
|
- EndStream: true,
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) writeData(streamID uint32, endStream bool, data []byte) {
|
|
|
- if err := st.fr.WriteData(streamID, endStream, data); err != nil {
|
|
|
- st.t.Fatalf("Error writing DATA: %v", err)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) readFrame() (Frame, error) {
|
|
|
- frc := make(chan Frame, 1)
|
|
|
- errc := make(chan error, 1)
|
|
|
- go func() {
|
|
|
- fr, err := st.fr.ReadFrame()
|
|
|
- if err != nil {
|
|
|
- errc <- err
|
|
|
- } else {
|
|
|
- frc <- fr
|
|
|
- }
|
|
|
- }()
|
|
|
- t := time.NewTimer(2 * time.Second)
|
|
|
- defer t.Stop()
|
|
|
- select {
|
|
|
- case f := <-frc:
|
|
|
- return f, nil
|
|
|
- case err := <-errc:
|
|
|
- return nil, err
|
|
|
- case <-t.C:
|
|
|
- return nil, errors.New("timeout waiting for frame")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) wantSettings() *SettingsFrame {
|
|
|
- f, err := st.readFrame()
|
|
|
- if err != nil {
|
|
|
- st.t.Fatalf("Error while expecting a SETTINGS frame: %v", err)
|
|
|
- }
|
|
|
- sf, ok := f.(*SettingsFrame)
|
|
|
- if !ok {
|
|
|
- st.t.Fatalf("got a %T; want *SettingsFrame", f)
|
|
|
- }
|
|
|
- return sf
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) wantPing() *PingFrame {
|
|
|
- f, err := st.readFrame()
|
|
|
- if err != nil {
|
|
|
- st.t.Fatalf("Error while expecting a PING frame: %v", err)
|
|
|
- }
|
|
|
- pf, ok := f.(*PingFrame)
|
|
|
- if !ok {
|
|
|
- st.t.Fatalf("got a %T; want *PingFrame", f)
|
|
|
- }
|
|
|
- return pf
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) wantRSTStream(streamID uint32, errCode ErrCode) {
|
|
|
- f, err := st.readFrame()
|
|
|
- if err != nil {
|
|
|
- st.t.Fatalf("Error while expecting an RSTStream frame: %v", err)
|
|
|
- }
|
|
|
- rs, ok := f.(*RSTStreamFrame)
|
|
|
- if !ok {
|
|
|
- st.t.Fatalf("got a %T; want *RSTStreamFrame", f)
|
|
|
- }
|
|
|
- if rs.FrameHeader.StreamID != streamID {
|
|
|
- st.t.Fatalf("RSTStream StreamID = %d; want %d", rs.FrameHeader.StreamID, streamID)
|
|
|
- }
|
|
|
- if rs.ErrCode != uint32(errCode) {
|
|
|
- st.t.Fatalf("RSTStream ErrCode = %d (%s); want %d (%s)", rs.ErrCode, rs.ErrCode, errCode, errCode)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) wantWindowUpdate(streamID, incr uint32) {
|
|
|
- f, err := st.readFrame()
|
|
|
- if err != nil {
|
|
|
- st.t.Fatalf("Error while expecting an RSTStream frame: %v", err)
|
|
|
- }
|
|
|
- wu, ok := f.(*WindowUpdateFrame)
|
|
|
- if !ok {
|
|
|
- st.t.Fatalf("got a %T; want *WindowUpdateFrame", f)
|
|
|
- }
|
|
|
- if wu.FrameHeader.StreamID != streamID {
|
|
|
- st.t.Fatalf("WindowUpdate StreamID = %d; want %d", wu.FrameHeader.StreamID, streamID)
|
|
|
- }
|
|
|
- if wu.Increment != incr {
|
|
|
- st.t.Fatalf("WindowUpdate increment = %d; want %d", wu.Increment, incr)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (st *serverTester) wantSettingsAck() {
|
|
|
- f, err := st.readFrame()
|
|
|
- if err != nil {
|
|
|
- st.t.Fatal(err)
|
|
|
- }
|
|
|
- sf, ok := f.(*SettingsFrame)
|
|
|
- if !ok {
|
|
|
- st.t.Fatalf("Wanting a settings ACK, received a %T", f)
|
|
|
- }
|
|
|
- if !sf.Header().Flags.Has(FlagSettingsAck) {
|
|
|
- st.t.Fatal("Settings Frame didn't have ACK set")
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer(t *testing.T) {
|
|
|
- gotReq := make(chan bool, 1)
|
|
|
- st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Foo", "Bar")
|
|
|
- gotReq <- true
|
|
|
- })
|
|
|
- defer st.Close()
|
|
|
-
|
|
|
- covers("3.5", `
|
|
|
- The server connection preface consists of a potentially empty
|
|
|
- SETTINGS frame ([SETTINGS]) that MUST be the first frame the
|
|
|
- server sends in the HTTP/2 connection.
|
|
|
- `)
|
|
|
-
|
|
|
- st.writePreface()
|
|
|
- st.writeInitialSettings()
|
|
|
- st.wantSettings().ForeachSetting(func(s Setting) error {
|
|
|
- t.Logf("Server sent setting %v = %v", s.ID, s.Val)
|
|
|
- return nil
|
|
|
- })
|
|
|
- st.writeSettingsAck()
|
|
|
- st.wantSettingsAck()
|
|
|
-
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t),
|
|
|
- EndStream: true, // no DATA frames
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
-
|
|
|
- select {
|
|
|
- case <-gotReq:
|
|
|
- case <-time.After(2 * time.Second):
|
|
|
- t.Error("timeout waiting for request")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Get(t *testing.T) {
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, "foo-bar", "some-value"),
|
|
|
- EndStream: true, // no DATA frames
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- }, func(r *http.Request) {
|
|
|
- if r.Method != "GET" {
|
|
|
- t.Errorf("Method = %q; want GET", r.Method)
|
|
|
- }
|
|
|
- if r.ContentLength != 0 {
|
|
|
- t.Errorf("ContentLength = %v; want 0", r.ContentLength)
|
|
|
- }
|
|
|
- if r.Close {
|
|
|
- t.Error("Close = true; want false")
|
|
|
- }
|
|
|
- if !strings.Contains(r.RemoteAddr, ":") {
|
|
|
- t.Errorf("RemoteAddr = %q; want something with a colon", r.RemoteAddr)
|
|
|
- }
|
|
|
- if r.Proto != "HTTP/2.0" || r.ProtoMajor != 2 || r.ProtoMinor != 0 {
|
|
|
- t.Errorf("Proto = %q Major=%v,Minor=%v; want HTTP/2.0", r.Proto, r.ProtoMajor, r.ProtoMinor)
|
|
|
- }
|
|
|
- wantHeader := http.Header{
|
|
|
- "Foo-Bar": []string{"some-value"},
|
|
|
- }
|
|
|
- if !reflect.DeepEqual(r.Header, wantHeader) {
|
|
|
- t.Errorf("Header = %#v; want %#v", r.Header, wantHeader)
|
|
|
- }
|
|
|
- if n, err := r.Body.Read([]byte(" ")); err != io.EOF || n != 0 {
|
|
|
- t.Errorf("Read = %d, %v; want 0, EOF", n, err)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// TODO: add a test with EndStream=true on the HEADERS but setting a
|
|
|
-// Content-Length anyway. Should we just omit it and force it to
|
|
|
-// zero?
|
|
|
-
|
|
|
-func TestServer_Request_Post_NoContentLength_EndStream(t *testing.T) {
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":method", "POST"),
|
|
|
- EndStream: true,
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- }, func(r *http.Request) {
|
|
|
- if r.Method != "POST" {
|
|
|
- t.Errorf("Method = %q; want POST", r.Method)
|
|
|
- }
|
|
|
- if r.ContentLength != 0 {
|
|
|
- t.Errorf("ContentLength = %v; want 0", r.ContentLength)
|
|
|
- }
|
|
|
- if n, err := r.Body.Read([]byte(" ")); err != io.EOF || n != 0 {
|
|
|
- t.Errorf("Read = %d, %v; want 0, EOF", n, err)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_ImmediateEOF(t *testing.T) {
|
|
|
- testBodyContents(t, -1, "", func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":method", "POST"),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, nil) // just kidding. empty body.
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_OneData(t *testing.T) {
|
|
|
- const content = "Some content"
|
|
|
- testBodyContents(t, -1, content, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":method", "POST"),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, []byte(content))
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_TwoData(t *testing.T) {
|
|
|
- const content = "Some content"
|
|
|
- testBodyContents(t, -1, content, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":method", "POST"),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, false, []byte(content[:5]))
|
|
|
- st.writeData(1, true, []byte(content[5:]))
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_ContentLength_Correct(t *testing.T) {
|
|
|
- const content = "Some content"
|
|
|
- testBodyContents(t, int64(len(content)), content, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t,
|
|
|
- ":method", "POST",
|
|
|
- "content-length", strconv.Itoa(len(content)),
|
|
|
- ),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, []byte(content))
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_ContentLength_TooLarge(t *testing.T) {
|
|
|
- testBodyContentsFail(t, 3, "Request declared a Content-Length of 3 but only wrote 2 bytes",
|
|
|
- func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t,
|
|
|
- ":method", "POST",
|
|
|
- "content-length", "3",
|
|
|
- ),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, []byte("12"))
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Post_Body_ContentLength_TooSmall(t *testing.T) {
|
|
|
- testBodyContentsFail(t, 4, "Sender tried to send more than declared Content-Length of 4 bytes",
|
|
|
- func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t,
|
|
|
- ":method", "POST",
|
|
|
- "content-length", "4",
|
|
|
- ),
|
|
|
- EndStream: false, // to say DATA frames are coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, []byte("12345"))
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func testBodyContents(t *testing.T, wantContentLength int64, wantBody string, write func(st *serverTester)) {
|
|
|
- testServerRequest(t, write, func(r *http.Request) {
|
|
|
- if r.Method != "POST" {
|
|
|
- t.Errorf("Method = %q; want POST", r.Method)
|
|
|
- }
|
|
|
- if r.ContentLength != wantContentLength {
|
|
|
- t.Errorf("ContentLength = %v; want %d", r.ContentLength, wantContentLength)
|
|
|
- }
|
|
|
- all, err := ioutil.ReadAll(r.Body)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if string(all) != wantBody {
|
|
|
- t.Errorf("Read = %q; want %q", all, wantBody)
|
|
|
- }
|
|
|
- if err := r.Body.Close(); err != nil {
|
|
|
- t.Fatalf("Close: %v", err)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func testBodyContentsFail(t *testing.T, wantContentLength int64, wantReadError string, write func(st *serverTester)) {
|
|
|
- testServerRequest(t, write, func(r *http.Request) {
|
|
|
- if r.Method != "POST" {
|
|
|
- t.Errorf("Method = %q; want POST", r.Method)
|
|
|
- }
|
|
|
- if r.ContentLength != wantContentLength {
|
|
|
- t.Errorf("ContentLength = %v; want %d", r.ContentLength, wantContentLength)
|
|
|
- }
|
|
|
- all, err := ioutil.ReadAll(r.Body)
|
|
|
- if err == nil {
|
|
|
- t.Fatalf("expected an error (%q) reading from the body. Successfully read %q instead.",
|
|
|
- wantReadError, all)
|
|
|
- }
|
|
|
- if !strings.Contains(err.Error(), wantReadError) {
|
|
|
- t.Fatalf("Body.Read = %v; want substring %q", err, wantReadError)
|
|
|
- }
|
|
|
- if err := r.Body.Close(); err != nil {
|
|
|
- t.Fatalf("Close: %v", err)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// Using a Host header, instead of :authority
|
|
|
-func TestServer_Request_Get_Host(t *testing.T) {
|
|
|
- const host = "example.com"
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, "host", host),
|
|
|
- EndStream: true,
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- }, func(r *http.Request) {
|
|
|
- if r.Host != host {
|
|
|
- t.Errorf("Host = %q; want %q", r.Host, host)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// Using an :authority pseudo-header, instead of Host
|
|
|
-func TestServer_Request_Get_Authority(t *testing.T) {
|
|
|
- const host = "example.com"
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":authority", host),
|
|
|
- EndStream: true,
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- }, func(r *http.Request) {
|
|
|
- if r.Host != host {
|
|
|
- t.Errorf("Host = %q; want %q", r.Host, host)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_WithContinuation(t *testing.T) {
|
|
|
- wantHeader := http.Header{
|
|
|
- "Foo-One": []string{"value-one"},
|
|
|
- "Foo-Two": []string{"value-two"},
|
|
|
- "Foo-Three": []string{"value-three"},
|
|
|
- }
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- fullHeaders := encodeHeader(t,
|
|
|
- "foo-one", "value-one",
|
|
|
- "foo-two", "value-two",
|
|
|
- "foo-three", "value-three",
|
|
|
- )
|
|
|
- remain := fullHeaders
|
|
|
- chunks := 0
|
|
|
- for len(remain) > 0 {
|
|
|
- const maxChunkSize = 5
|
|
|
- chunk := remain
|
|
|
- if len(chunk) > maxChunkSize {
|
|
|
- chunk = chunk[:maxChunkSize]
|
|
|
- }
|
|
|
- remain = remain[len(chunk):]
|
|
|
-
|
|
|
- if chunks == 0 {
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: chunk,
|
|
|
- EndStream: true, // no DATA frames
|
|
|
- EndHeaders: false, // we'll have continuation frames
|
|
|
- })
|
|
|
- } else {
|
|
|
- err := st.fr.WriteContinuation(1, len(remain) == 0, chunk)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- }
|
|
|
- chunks++
|
|
|
- }
|
|
|
- if chunks < 2 {
|
|
|
- t.Fatal("too few chunks")
|
|
|
- }
|
|
|
- }, func(r *http.Request) {
|
|
|
- if !reflect.DeepEqual(r.Header, wantHeader) {
|
|
|
- t.Errorf("Header = %#v; want %#v", r.Header, wantHeader)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-// Concatenated cookie headers. ("8.1.2.5 Compressing the Cookie Header Field")
|
|
|
-func TestServer_Request_CookieConcat(t *testing.T) {
|
|
|
- const host = "example.com"
|
|
|
- testServerRequest(t, func(st *serverTester) {
|
|
|
- st.bodylessReq1(
|
|
|
- ":authority", host,
|
|
|
- "cookie", "a=b",
|
|
|
- "cookie", "c=d",
|
|
|
- "cookie", "e=f",
|
|
|
- )
|
|
|
- }, func(r *http.Request) {
|
|
|
- const want = "a=b; c=d; e=f"
|
|
|
- if got := r.Header.Get("Cookie"); got != want {
|
|
|
- t.Errorf("Cookie = %q; want %q", got, want)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_CapitalHeader(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1("UPPER", "v") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_Missing_method(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":method", "") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_ExactlyOne(t *testing.T) {
|
|
|
- // 8.1.2.3 Request Pseudo-Header Fields
|
|
|
- // "All HTTP/2 requests MUST include exactly one valid value" ...
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":method", "GET", ":method", "POST") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_AfterRegular(t *testing.T) {
|
|
|
- // 8.1.2.3 Request Pseudo-Header Fields
|
|
|
- // "All pseudo-header fields MUST appear in the header block
|
|
|
- // before regular header fields. Any request or response that
|
|
|
- // contains a pseudo-header field that appears in a header
|
|
|
- // block after a regular header field MUST be treated as
|
|
|
- // malformed (Section 8.1.2.6)."
|
|
|
- testRejectRequest(t, func(st *serverTester) {
|
|
|
- var buf bytes.Buffer
|
|
|
- enc := hpack.NewEncoder(&buf)
|
|
|
- enc.WriteField(hpack.HeaderField{Name: ":method", Value: "GET"})
|
|
|
- enc.WriteField(hpack.HeaderField{Name: "regular", Value: "foobar"})
|
|
|
- enc.WriteField(hpack.HeaderField{Name: ":path", Value: "/"})
|
|
|
- enc.WriteField(hpack.HeaderField{Name: ":scheme", Value: "https"})
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: buf.Bytes(),
|
|
|
- EndStream: true,
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_Missing_path(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":path", "") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_Missing_scheme(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":scheme", "") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_scheme_invalid(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":scheme", "bogus") })
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Request_Reject_Pseudo_Unknown(t *testing.T) {
|
|
|
- testRejectRequest(t, func(st *serverTester) { st.bodylessReq1(":unknown_thing", "") })
|
|
|
-}
|
|
|
-
|
|
|
-func testRejectRequest(t *testing.T, send func(*serverTester)) {
|
|
|
- st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
- t.Fatal("server request made it to handler; should've been rejected")
|
|
|
- })
|
|
|
- defer st.Close()
|
|
|
-
|
|
|
- st.greet()
|
|
|
- send(st)
|
|
|
- st.wantRSTStream(1, ErrCodeProtocol)
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Ping(t *testing.T) {
|
|
|
- st := newServerTester(t, nil)
|
|
|
- defer st.Close()
|
|
|
- st.greet()
|
|
|
-
|
|
|
- // Server should ignore this one, since it has ACK set.
|
|
|
- ackPingData := [8]byte{1, 2, 4, 8, 16, 32, 64, 128}
|
|
|
- if err := st.fr.WritePing(true, ackPingData); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- // But the server should reply to this one, since ACK is false.
|
|
|
- pingData := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
|
|
|
- if err := st.fr.WritePing(false, pingData); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- pf := st.wantPing()
|
|
|
- if !pf.Flags.Has(FlagPingAck) {
|
|
|
- t.Error("response ping doesn't have ACK set")
|
|
|
- }
|
|
|
- if pf.Data != pingData {
|
|
|
- t.Errorf("response ping has data %q; want %q", pf.Data, pingData)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func TestServer_Handler_Sends_WindowUpdate(t *testing.T) {
|
|
|
- puppet := newHandlerPuppet()
|
|
|
- st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
- puppet.act(w, r)
|
|
|
- })
|
|
|
- defer st.Close()
|
|
|
- defer puppet.done()
|
|
|
-
|
|
|
- st.greet()
|
|
|
-
|
|
|
- st.writeHeaders(HeadersFrameParam{
|
|
|
- StreamID: 1, // clients send odd numbers
|
|
|
- BlockFragment: encodeHeader(t, ":method", "POST"),
|
|
|
- EndStream: false, // data coming
|
|
|
- EndHeaders: true,
|
|
|
- })
|
|
|
- st.writeData(1, true, []byte("abcdef"))
|
|
|
- puppet.do(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- buf := make([]byte, 3)
|
|
|
- _, err := io.ReadFull(r.Body, buf)
|
|
|
- if err != nil {
|
|
|
- t.Error(err)
|
|
|
- return
|
|
|
- }
|
|
|
- if string(buf) != "abc" {
|
|
|
- t.Errorf("read %q; want abc", buf)
|
|
|
- }
|
|
|
- })
|
|
|
- st.wantWindowUpdate(0, 3)
|
|
|
- st.wantWindowUpdate(1, 3)
|
|
|
- puppet.do(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- buf := make([]byte, 3)
|
|
|
- _, err := io.ReadFull(r.Body, buf)
|
|
|
- if err != nil {
|
|
|
- t.Error(err)
|
|
|
- return
|
|
|
- }
|
|
|
- if string(buf) != "def" {
|
|
|
- t.Errorf("read %q; want abc", buf)
|
|
|
- }
|
|
|
- })
|
|
|
- st.wantWindowUpdate(0, 3)
|
|
|
- st.wantWindowUpdate(1, 3)
|
|
|
-}
|
|
|
-
|
|
|
-// TODO: test HEADERS w/o EndHeaders + another HEADERS (should get rejected)
|
|
|
-// TODO: test HEADERS w/ EndHeaders + a continuation HEADERS (should get rejected)
|
|
|
-
|
|
|
-// testServerRequest sets up an idle HTTP/2 connection and lets you
|
|
|
-// write a single request with writeReq, and then verify that the
|
|
|
-// *http.Request is built correctly in checkReq.
|
|
|
-func testServerRequest(t *testing.T, writeReq func(*serverTester), checkReq func(*http.Request)) {
|
|
|
- gotReq := make(chan bool, 1)
|
|
|
- st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
- if r.Body == nil {
|
|
|
- t.Fatal("nil Body")
|
|
|
- }
|
|
|
- checkReq(r)
|
|
|
- gotReq <- true
|
|
|
- })
|
|
|
- defer st.Close()
|
|
|
-
|
|
|
- st.greet()
|
|
|
- writeReq(st)
|
|
|
-
|
|
|
- select {
|
|
|
- case <-gotReq:
|
|
|
- case <-time.After(2 * time.Second):
|
|
|
- t.Error("timeout waiting for request")
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-func TestServerWithCurl(t *testing.T) {
|
|
|
- requireCurl(t)
|
|
|
-
|
|
|
- ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- // TODO: add a bunch of different tests with different
|
|
|
- // behavior, as a function of r or a table.
|
|
|
- // -- with request body, without.
|
|
|
- // -- no interaction with w.
|
|
|
- // -- panic
|
|
|
- // -- modify Header only, but no writes or writeheader (this test)
|
|
|
- // -- WriteHeader only
|
|
|
- // -- Write only
|
|
|
- // -- WriteString
|
|
|
- // -- both
|
|
|
- // -- huge headers over a frame size so we get continuation headers.
|
|
|
- // Look at net/http's Server tests for inspiration.
|
|
|
- w.Header().Set("Foo", "Bar")
|
|
|
- }))
|
|
|
- ConfigureServer(ts.Config, &Server{})
|
|
|
- ts.TLS = ts.Config.TLSConfig // the httptest.Server has its own copy of this TLS config
|
|
|
- ts.StartTLS()
|
|
|
- defer ts.Close()
|
|
|
-
|
|
|
- var gotConn int32
|
|
|
- testHookOnConn = func() { atomic.StoreInt32(&gotConn, 1) }
|
|
|
-
|
|
|
- t.Logf("Running test server for curl to hit at: %s", ts.URL)
|
|
|
- container := curl(t, "--silent", "--http2", "--insecure", "-v", ts.URL)
|
|
|
- defer kill(container)
|
|
|
- resc := make(chan interface{}, 1)
|
|
|
- go func() {
|
|
|
- res, err := dockerLogs(container)
|
|
|
- if err != nil {
|
|
|
- resc <- err
|
|
|
- } else {
|
|
|
- resc <- res
|
|
|
- }
|
|
|
- }()
|
|
|
- select {
|
|
|
- case res := <-resc:
|
|
|
- if err, ok := res.(error); ok {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if !strings.Contains(string(res.([]byte)), "< foo:Bar") {
|
|
|
- t.Errorf("didn't see foo:Bar header")
|
|
|
- t.Logf("Got: %s", res)
|
|
|
- }
|
|
|
- case <-time.After(3 * time.Second):
|
|
|
- t.Errorf("timeout waiting for curl")
|
|
|
- }
|
|
|
-
|
|
|
- if atomic.LoadInt32(&gotConn) == 0 {
|
|
|
- t.Error("never saw an http2 connection")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func dockerLogs(container string) ([]byte, error) {
|
|
|
- out, err := exec.Command("docker", "wait", container).CombinedOutput()
|
|
|
- if err != nil {
|
|
|
- return out, err
|
|
|
- }
|
|
|
- exitStatus, err := strconv.Atoi(strings.TrimSpace(string(out)))
|
|
|
- if err != nil {
|
|
|
- return out, errors.New("unexpected exit status from docker wait")
|
|
|
- }
|
|
|
- out, err = exec.Command("docker", "logs", container).CombinedOutput()
|
|
|
- exec.Command("docker", "rm", container).Run()
|
|
|
- if err == nil && exitStatus != 0 {
|
|
|
- err = fmt.Errorf("exit status %d", exitStatus)
|
|
|
- }
|
|
|
- return out, err
|
|
|
-}
|
|
|
-
|
|
|
-func kill(container string) {
|
|
|
- exec.Command("docker", "kill", container).Run()
|
|
|
- exec.Command("docker", "rm", container).Run()
|
|
|
-}
|
|
|
-
|
|
|
-// Verify that curl has http2.
|
|
|
-func requireCurl(t *testing.T) {
|
|
|
- out, err := dockerLogs(curl(t, "--version"))
|
|
|
- if err != nil {
|
|
|
- t.Skipf("failed to determine curl features; skipping test")
|
|
|
- }
|
|
|
- if !strings.Contains(string(out), "HTTP2") {
|
|
|
- t.Skip("curl doesn't support HTTP2; skipping test")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func curl(t *testing.T, args ...string) (container string) {
|
|
|
- out, err := exec.Command("docker", append([]string{"run", "-d", "--net=host", "gohttp2/curl"}, args...)...).CombinedOutput()
|
|
|
- if err != nil {
|
|
|
- t.Skipf("Failed to run curl in docker: %v, %s", err, out)
|
|
|
- }
|
|
|
- return strings.TrimSpace(string(out))
|
|
|
-}
|
|
|
-
|
|
|
type twriter struct {
|
|
|
t testing.TB
|
|
|
}
|
|
|
@@ -869,6 +80,25 @@ func encodeHeader(t *testing.T, headers ...string) []byte {
|
|
|
return buf.Bytes()
|
|
|
}
|
|
|
|
|
|
+// Verify that curl has http2.
|
|
|
+func requireCurl(t *testing.T) {
|
|
|
+ out, err := dockerLogs(curl(t, "--version"))
|
|
|
+ if err != nil {
|
|
|
+ t.Skipf("failed to determine curl features; skipping test")
|
|
|
+ }
|
|
|
+ if !strings.Contains(string(out), "HTTP2") {
|
|
|
+ t.Skip("curl doesn't support HTTP2; skipping test")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func curl(t *testing.T, args ...string) (container string) {
|
|
|
+ out, err := exec.Command("docker", append([]string{"run", "-d", "--net=host", "gohttp2/curl"}, args...)...).CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ t.Skipf("Failed to run curl in docker: %v, %s", err, out)
|
|
|
+ }
|
|
|
+ return strings.TrimSpace(string(out))
|
|
|
+}
|
|
|
+
|
|
|
type puppetCommand struct {
|
|
|
fn func(w http.ResponseWriter, r *http.Request)
|
|
|
done chan<- bool
|
|
|
@@ -897,3 +127,24 @@ func (p *handlerPuppet) do(fn func(http.ResponseWriter, *http.Request)) {
|
|
|
p.ch <- puppetCommand{fn, done}
|
|
|
<-done
|
|
|
}
|
|
|
+func dockerLogs(container string) ([]byte, error) {
|
|
|
+ out, err := exec.Command("docker", "wait", container).CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ return out, err
|
|
|
+ }
|
|
|
+ exitStatus, err := strconv.Atoi(strings.TrimSpace(string(out)))
|
|
|
+ if err != nil {
|
|
|
+ return out, errors.New("unexpected exit status from docker wait")
|
|
|
+ }
|
|
|
+ out, err = exec.Command("docker", "logs", container).CombinedOutput()
|
|
|
+ exec.Command("docker", "rm", container).Run()
|
|
|
+ if err == nil && exitStatus != 0 {
|
|
|
+ err = fmt.Errorf("exit status %d", exitStatus)
|
|
|
+ }
|
|
|
+ return out, err
|
|
|
+}
|
|
|
+
|
|
|
+func kill(container string) {
|
|
|
+ exec.Command("docker", "kill", container).Run()
|
|
|
+ exec.Command("docker", "rm", container).Run()
|
|
|
+}
|