// Copyright 2024 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 http3 import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "maps" "math" "net" "net/http" "reflect" "slices" "testing" "testing/synctest" "time" "golang.org/x/net/quic" ) // unusablePacketConn is a net.PacketConn whose LocalAddr is not a valid // UDP address, which makes quic.NewEndpoint fail. type unusablePacketConn struct { net.PacketConn closed bool } func (c *unusablePacketConn) LocalAddr() net.Addr { return &net.UnixAddr{Name: "unusable", Net: "unix"} } func (c *unusablePacketConn) Close() error { c.closed = true return nil } // TestTransportInitEndpointError verifies that a transport which fails to // create its QUIC endpoint reports the error, rather than proceeding with a // nil endpoint. func TestTransportInitEndpointError(t *testing.T) { conn := &unusablePacketConn{} tr := &transport{ tr1: new(http.Transport), // TLSClientConfig is nil, as by default opts: TransportOpts{ ListenPacket: func(network, addr string) (net.PacketConn, error) { return conn, nil }, }, activeConns: make(map[*clientConn]struct{}), } if err := tr.initEndpoint(); err == nil { t.Fatal("initEndpoint() = nil, want error") } if tr.endpoint != nil { t.Errorf("after failed initEndpoint, transport.endpoint = %v, want nil", tr.endpoint) } if !conn.closed { t.Errorf("after failed initEndpoint, the net.PacketConn was not closed") } // dial must report the error rather than panicking on the nil endpoint. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if _, err := tr.dial(ctx, "127.0.0.1:443", &tls.Config{}, nil); err == nil { t.Fatal("dial() = nil error, want error") } } func TestTransportServerCreatesBidirectionalStream(t *testing.T) { // "Clients MUST treat receipt of a server-initiated bidirectional // stream as a connection error of type H3_STREAM_CREATION_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3 synctest.Test(t, func(t *testing.T) { tc := newTestClientConn(t) tc.greet() st := tc.newStream(streamTypeRequest) st.Flush() tc.wantClosed("after server creates bidi stream", errH3StreamCreationError) }) } func TestClientConnMethods(t *testing.T) { synctest.Test(t, func(t *testing.T) { var called bool hook := func() { if called { t.Error("state hook was unexpectedly called") } called = true } verifyHookWasCalled := func() { if !called { t.Error("state hook was unexpectedly not called") } called = false } tc := newTestClientConnWithHook(t, hook) tc.greet() // Initial state after establishing connection. if err := tc.cc.Err(); err != nil { t.Errorf("cc.Err() = %v, want nil", err) } if avail := tc.cc.Available(); avail != math.MaxInt { t.Errorf("cc.Available() = %v, want %v", avail, math.MaxInt) } if inFlight := tc.cc.InFlight(); inFlight != 0 { t.Errorf("cc.InFlight() = %v, want 0", inFlight) } // Release, with Reserve before and without. for _, reserveBefore := range []bool{true, false} { if reserveBefore { if err := tc.cc.Reserve(); err != nil { t.Fatalf("cc.Reserve() failed: %v", err) } if avail := tc.cc.Available(); avail != math.MaxInt { t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt) } if inFlight := tc.cc.InFlight(); inFlight != 1 { t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight) } } tc.cc.Release() if avail := tc.cc.Available(); avail != math.MaxInt { t.Errorf("after Release, cc.Available() = %v, want %v", avail, math.MaxInt) } if inFlight := tc.cc.InFlight(); inFlight != 0 { t.Errorf("after Release, cc.InFlight() = %v, want 0", inFlight) } } // RoundTrip, with Reserve before and without. for _, reserveBefore := range []bool{true, false} { if reserveBefore { if err := tc.cc.Reserve(); err != nil { t.Fatalf("cc.Reserve() failed: %v", err) } if avail := tc.cc.Available(); avail != math.MaxInt { t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt) } if inFlight := tc.cc.InFlight(); inFlight != 1 { t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight) } } req, _ := http.NewRequest("GET", "https://example.com/", nil) rt := tc.roundTrip(req) if avail := tc.cc.Available(); avail != math.MaxInt { t.Errorf("after RoundTrip, cc.Available() = %v, want %v", avail, math.MaxInt) } st := tc.wantStream(streamTypeRequest) st.wantHeaders(nil) st.writeHeaders(http.Header{":status": []string{"200"}}) resp := rt.response() if resp.StatusCode != 200 { t.Errorf("resp.StatusCode = %v, want 200", resp.StatusCode) } if inFlight := tc.cc.InFlight(); inFlight != 1 { // InFlight should decrement only after the body is closed. t.Errorf("before body close, cc.InFlight() = %v, want 1", inFlight) } resp.Body.Close() if inFlight := tc.cc.InFlight(); inFlight != 0 { t.Errorf("after body close, cc.InFlight() = %v, want 0", inFlight) } verifyHookWasCalled() } // Connection closure. if err := tc.cc.Reserve(); err != nil { t.Fatalf("cc.Reserve() failed: %v", err) } tc.cc.Close() synctest.Wait() verifyHookWasCalled() if err := tc.cc.Err(); err == nil { t.Error("after connection is closed, cc.Err() = nil, want err") } if avail := tc.cc.Available(); avail != 0 { t.Errorf("after connection is closed, cc.Available() = %v, want 0", avail) } if inFlight := tc.cc.InFlight(); inFlight != 0 { t.Errorf("after connection is closed, cc.InFlight() = %v, want 0", inFlight) } }) } // A testQUICConn wraps a *quic.Conn and provides methods for inspecting it. type testQUICConn struct { t testing.TB qconn *quic.Conn streams map[streamType][]*testQUICStream } func newTestQUICConn(t testing.TB, qconn *quic.Conn) *testQUICConn { tq := &testQUICConn{ t: t, qconn: qconn, streams: make(map[streamType][]*testQUICStream), } go tq.acceptStreams(t.Context()) t.Cleanup(func() { tq.qconn.Close() }) return tq } func (tq *testQUICConn) acceptStreams(ctx context.Context) { for { qst, err := tq.qconn.AcceptStream(ctx) if err != nil { return } st := newStream(qst) stype := streamTypeRequest if qst.IsReadOnly() { v, err := st.readVarint() if err != nil { tq.t.Errorf("error reading stream type from unidirectional stream: %v", err) continue } stype = streamType(v) } tq.streams[stype] = append(tq.streams[stype], newTestQUICStream(tq.t, st)) } } func (tq *testQUICConn) newStream(stype streamType) *testQUICStream { tq.t.Helper() var qs *quic.Stream var err error if stype == streamTypeRequest { qs, err = tq.qconn.NewStream(canceledCtx) } else { qs, err = tq.qconn.NewSendOnlyStream(canceledCtx) } if err != nil { tq.t.Fatal(err) } st := newStream(qs) if stype != streamTypeRequest { st.writeVarint(int64(stype)) if err := st.Flush(); err != nil { tq.t.Fatal(err) } } return newTestQUICStream(tq.t, st) } // wantNotClosed asserts that the peer has not closed the connection. func (tq *testQUICConn) wantNotClosed(reason string) { t := tq.t t.Helper() synctest.Wait() err := tq.qconn.Wait(canceledCtx) if !errors.Is(err, context.Canceled) { t.Fatalf("%v: want QUIC connection to be alive; closed with error: %v", reason, err) } } // wantClosed asserts that the peer has closed the connection // with the provided error code. func (tq *testQUICConn) wantClosed(reason string, want error) { t := tq.t t.Helper() synctest.Wait() if e, ok := want.(http3Error); ok { want = &quic.ConnectionCloseError{Code: uint64(e)} } got := tq.qconn.Wait(canceledCtx) if errors.Is(got, context.Canceled) { t.Fatalf("%v: want QUIC connection closed, but it is not", reason) } if !errors.Is(got, want) { t.Fatalf("%v: connection closed with error: %v; want %v", reason, got, want) } } // wantStream asserts that a stream of a given type has been created, // and returns that stream. func (tq *testQUICConn) wantStream(stype streamType) *testQUICStream { tq.t.Helper() synctest.Wait() if len(tq.streams[stype]) == 0 { tq.t.Fatalf("expected a %v stream to be created, but none were", stype) } ts := tq.streams[stype][0] tq.streams[stype] = tq.streams[stype][1:] return ts } // testQUICStream wraps a QUIC stream and provides methods for inspecting it. type testQUICStream struct { t testing.TB *stream } func newTestQUICStream(t testing.TB, st *stream) *testQUICStream { st.stream.SetReadContext(canceledCtx) st.stream.SetWriteContext(canceledCtx) return &testQUICStream{ t: t, stream: st, } } func (ts *testQUICStream) wantIdle(reason string) { ts.t.Helper() synctest.Wait() qs := ts.stream.stream qs.SetReadContext(canceledCtx) if _, err := qs.Read(make([]byte, 1)); !errors.Is(err, context.Canceled) { ts.t.Fatalf("%v: want stream to be idle, but stream has content", reason) } qs.SetReadContext(context.Background()) } // wantFrameHeader calls readFrameHeader and asserts that the frame is of a given type. func (ts *testQUICStream) wantFrameHeader(reason string, wantType frameType) { ts.t.Helper() synctest.Wait() gotType, err := ts.readFrameHeader() if err != nil { ts.t.Fatalf("%v: failed to read frame header: %v", reason, err) } if gotType != wantType { ts.t.Fatalf("%v: got frame type %v, want %v", reason, gotType, wantType) } } // wantHeaders reads a HEADERS frame. // If want is nil, the contents of the frame are ignored. func (ts *testQUICStream) wantHeaders(want http.Header) { ts.t.Helper() synctest.Wait() ftype, err := ts.readFrameHeader() if err != nil { ts.t.Fatalf("want HEADERS frame, got error: %v", err) } if ftype != frameTypeHeaders { ts.t.Fatalf("want HEADERS frame, got: %v", ftype) } if want == nil { if err := ts.discardFrame(); err != nil { ts.t.Fatalf("discardFrame: %v", err) } return } got := make(http.Header) var dec qpackDecoder err = dec.decode(ts.stream, func(_ indexType, name, value string) error { got.Add(name, value) return nil }) if diff := diffHeaders(got, want); diff != "" { ts.t.Fatalf("unexpected response headers:\n%v", diff) } if err := ts.endFrame(); err != nil { ts.t.Fatalf("endFrame: %v", err) } } // wantSomeHeaders reads a HEADERS frame and asserts that want is a subset of // the read HEADERS frame. // This is like wantHeaders, but headers that are in the HEADERS frame but not // in want are ignored. func (ts *testQUICStream) wantSomeHeaders(want http.Header) { ts.t.Helper() synctest.Wait() ftype, err := ts.readFrameHeader() if err != nil { ts.t.Fatalf("want HEADERS frame, got error: %v", err) } if ftype != frameTypeHeaders { ts.t.Fatalf("want HEADERS frame, got: %v", ftype) } if want == nil { panic("use wantHeaders(nil) instead to ignore the content of the frame") } got := make(http.Header) var dec qpackDecoder err = dec.decode(ts.stream, func(_ indexType, name, value string) error { got.Add(name, value) return nil }) for name := range got { if _, ok := want[name]; !ok { delete(got, name) } } if diff := diffHeaders(got, want); diff != "" { ts.t.Fatalf("unexpected response headers:\n%v", diff) } if err := ts.endFrame(); err != nil { ts.t.Fatalf("endFrame: %v", err) } } func (ts *testQUICStream) encodeHeaders(h http.Header) []byte { ts.t.Helper() var enc qpackEncoder return enc.encode(func(yield func(itype indexType, name, value string)) { names := slices.Collect(maps.Keys(h)) slices.Sort(names) for _, k := range names { for _, v := range h[k] { yield(mayIndex, k, v) } } }) } func (ts *testQUICStream) writeHeaders(h http.Header) { ts.t.Helper() headers := ts.encodeHeaders(h) ts.writeVarint(int64(frameTypeHeaders)) ts.writeVarint(int64(len(headers))) ts.Write(headers) if err := ts.Flush(); err != nil { ts.t.Fatalf("flushing HEADERS frame: %v", err) } } // writeHeadersRaw is just like writeHeaders, but avoids qpackEncoder.encode, // which will automatically make sure that headers are encoded correctly, e.g. // making field names all lowercase, skipping non-ASCII names. // This method can be used to test that we properly reject invalid headers. func (ts *testQUICStream) writeHeadersRaw(h http.Header) { ts.t.Helper() var b []byte b = appendPrefixedInt(b, 0, 8, 0) // Required Insert Count b = appendPrefixedInt(b, 0, 7, 0) // Delta Base names := slices.Collect(maps.Keys(h)) slices.Sort(names) for _, k := range names { for _, v := range h[k] { if i, ok := staticTableByNameValue[tableEntry{k, v}]; ok { b = appendIndexedFieldLine(b, staticTable, i) } else if i, ok := staticTableByName[k]; ok { b = appendLiteralFieldLineWithNameReference(b, staticTable, mayIndex, i, v) } else { b = appendLiteralFieldLineWithLiteralName(b, mayIndex, k, v) } } } headers := b ts.writeVarint(int64(frameTypeHeaders)) ts.writeVarint(int64(len(headers))) ts.Write(headers) if err := ts.Flush(); err != nil { ts.t.Fatalf("flushing HEADERS frame: %v", err) } } func (ts *testQUICStream) writeData(b []byte) { ts.t.Helper() ts.writeVarint(int64(frameTypeData)) ts.writeVarint(int64(len(b))) ts.Write(b) if err := ts.Flush(); err != nil { ts.t.Fatalf("flushing DATA frame: %v", err) } } func (ts *testQUICStream) wantData(want []byte) { ts.t.Helper() synctest.Wait() ftype, err := ts.readFrameHeader() if err != nil { ts.t.Fatalf("want DATA frame, got error: %v", err) } if ftype != frameTypeData { ts.t.Fatalf("want DATA frame, got: %v", ftype) } got, err := ts.readFrameData() if err != nil { ts.t.Fatalf("error reading DATA frame: %v", err) } if !bytes.Equal(got, want) { ts.t.Fatalf("got data: {%x}, want {%x}", got, want) } if err := ts.endFrame(); err != nil { ts.t.Fatalf("endFrame: %v", err) } } func (ts *testQUICStream) wantClosed(reason string) { ts.t.Helper() synctest.Wait() ftype, err := ts.readFrameHeader() if err != io.EOF { ts.t.Fatalf("%v: want io.EOF, got %v %v", reason, ftype, err) } } func (ts *testQUICStream) wantError(want quic.StreamError) { ts.t.Helper() synctest.Wait() _, err := ts.ReadByte() if err == nil { ts.t.Fatalf("successfully read from stream; want stream error code %v", want) } var got quic.StreamError if !errors.As(err, &got) { ts.t.Fatalf("stream error = %v; want %v", err, want) } if got != want { ts.t.Fatalf("stream error code = %v; want %v", got, want) } } func (ts *testQUICStream) wantSettings(f func(settingType, value int64) error) { ts.t.Helper() synctest.Wait() if f == nil { f = func(settingType, value int64) error { return nil } } if err := ts.readSettings(f); err != nil { ts.t.Fatalf("f returned an error: %v", err) } } func (ts *testQUICStream) wantGoaway(wantID int64) { ts.t.Helper() synctest.Wait() ftype, err := ts.readFrameHeader() if err != nil { ts.t.Fatalf("want GOAWAY frame, got error: %v", err) } if ftype != frameTypeGoaway { ts.t.Fatalf("want GOAWAY frame, got: %v", ftype) } gotID, err := ts.readVarint() if err != nil { ts.t.Fatalf("failed reading GOAWAY frame, got error: %v", err) } if gotID != wantID { ts.t.Fatalf("got stream ID %v from GOAWAY frame, want %v stream ID", gotID, wantID) } } func (ts *testQUICStream) writePushPromise(pushID int64, h http.Header) { ts.t.Helper() headers := ts.encodeHeaders(h) ts.writeVarint(int64(frameTypePushPromise)) ts.writeVarint(int64(sizeVarint(uint64(pushID)) + len(headers))) ts.writeVarint(pushID) ts.Write(headers) if err := ts.Flush(); err != nil { ts.t.Fatalf("flushing PUSH_PROMISE frame: %v", err) } } func diffHeaders(got, want http.Header) string { // nil and 0-length non-nil are equal. if len(got) == 0 && len(want) == 0 { return "" } // We could do a more sophisticated diff here. // DeepEqual is good enough for now. if reflect.DeepEqual(got, want) { return "" } return fmt.Sprintf("got: %v\nwant: %v", got, want) } func (ts *testQUICStream) Flush() error { err := ts.stream.Flush() ts.t.Helper() if err != nil { ts.t.Errorf("unexpected error flushing stream: %v", err) } return err } // A testClientConn is a ClientConn on a test network. type testClientConn struct { tr *transport cc *clientConn // *testQUICConn is the server half of the connection. *testQUICConn control *testQUICStream } func newTestClientConnWithHook(t testing.TB, stateHook func()) *testClientConn { e1, e2 := newQUICEndpointPair(t) tr := &transport{ endpoint: e1, tr1: new(http.Transport), activeConns: make(map[*clientConn]struct{}), } cc, err := tr.dial(t.Context(), e2.LocalAddr().String(), testTLSConfig, stateHook) if err != nil { t.Fatal(err) } t.Cleanup(func() { cc.Close() }) srvConn, err := e2.Accept(t.Context()) if err != nil { t.Fatal(err) } tc := &testClientConn{ tr: tr, cc: cc, testQUICConn: newTestQUICConn(t, srvConn), } synctest.Wait() return tc } func newTestClientConn(t testing.TB) *testClientConn { return newTestClientConnWithHook(t, nil) } // greet performs initial connection handshaking with the client. func (tc *testClientConn) greet() { // Client creates a control stream. clientControlStream := tc.wantStream(streamTypeControl) clientControlStream.wantFrameHeader( "client sends SETTINGS frame on control stream", frameTypeSettings) clientControlStream.discardFrame() // Server creates a control stream. tc.control = tc.newStream(streamTypeControl) tc.control.writeVarint(int64(frameTypeSettings)) tc.control.writeVarint(0) // size tc.control.Flush() synctest.Wait() } type testRoundTrip struct { t testing.TB resp *http.Response respErr error } func (rt *testRoundTrip) done() bool { synctest.Wait() return rt.resp != nil || rt.respErr != nil } func (rt *testRoundTrip) result() (*http.Response, error) { rt.t.Helper() if !rt.done() { rt.t.Fatal("RoundTrip is not done; want it to be") } return rt.resp, rt.respErr } func (rt *testRoundTrip) response() *http.Response { rt.t.Helper() if !rt.done() { rt.t.Fatal("RoundTrip is not done; want it to be") } if rt.respErr != nil { rt.t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr) } return rt.resp } // err returns the (possibly nil) error result of RoundTrip. func (rt *testRoundTrip) err() error { rt.t.Helper() _, err := rt.result() return err } func (rt *testRoundTrip) wantError(reason string) { rt.t.Helper() synctest.Wait() if !rt.done() { rt.t.Fatalf("%v: RoundTrip is not done; want it to have returned an error", reason) } if rt.respErr == nil { rt.t.Fatalf("%v: RoundTrip succeeded; want it to have returned an error", reason) } } // wantStatus indicates the expected response StatusCode. func (rt *testRoundTrip) wantStatus(want int) { rt.t.Helper() if got := rt.response().StatusCode; got != want { rt.t.Fatalf("got response status %v, want %v", got, want) } } func (rt *testRoundTrip) wantHeaders(want http.Header) { rt.t.Helper() if diff := diffHeaders(rt.response().Header, want); diff != "" { rt.t.Fatalf("unexpected response headers:\n%v", diff) } } func (rt *testRoundTrip) wantTrailers(want http.Header) { rt.t.Helper() if diff := diffHeaders(rt.response().Trailer, want); diff != "" { rt.t.Fatalf("unexpected response trailers:\n%v", diff) } } // readBody reads the contents of the response body. func (rt *testRoundTrip) readBody() ([]byte, error) { t := rt.t t.Helper() return io.ReadAll(rt.response().Body) } // wantBody consumes the a body and asserts that it is as expected. func (rt *testRoundTrip) wantBody(want []byte) { t := rt.t t.Helper() got, err := rt.readBody() if err != nil { t.Fatalf("unexpected error reading response body: %v", err) } if !bytes.Equal(got, want) { t.Fatalf("unexpected response body:\ngot: %q\nwant: %q", got, want) } } func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip { rt := &testRoundTrip{t: tc.t} go func() { rt.resp, rt.respErr = tc.cc.RoundTrip(req) }() return rt }