package self_test import ( "bufio" "bytes" "compress/gzip" "context" "crypto/tls" "encoding/binary" "errors" "fmt" "io" "net" "net/http" "net/http/httptrace" "net/textproto" "net/url" "os" "strconv" "sync/atomic" "time" "golang.org/x/sync/errgroup" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" ) type neverEnding byte func (b neverEnding) Read(p []byte) (n int, err error) { for i := range p { p[i] = byte(b) } return len(p), nil } const deadlineDelay = 250 * time.Millisecond var _ = Describe("HTTP tests", func() { var ( mux *http.ServeMux client *http.Client rt *http3.RoundTripper server *http3.Server stoppedServing chan struct{} port int ) BeforeEach(func() { mux = http.NewServeMux() mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() io.WriteString(w, "Hello, World!\n") // don't check the error here. Stream may be reset. }) mux.HandleFunc("/prdata", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() sl := r.URL.Query().Get("len") if sl != "" { var err error l, err := strconv.Atoi(sl) Expect(err).NotTo(HaveOccurred()) w.Write(GeneratePRData(l)) // don't check the error here. Stream may be reset. } else { w.Write(PRData) // don't check the error here. Stream may be reset. } }) mux.HandleFunc("/prdatalong", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Write(PRDataLong) // don't check the error here. Stream may be reset. }) mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() body, err := io.ReadAll(r.Body) Expect(err).NotTo(HaveOccurred()) w.Write(body) // don't check the error here. Stream may be reset. }) mux.HandleFunc("/remoteAddr", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Header().Set("X-RemoteAddr", r.RemoteAddr) w.WriteHeader(http.StatusOK) }) server = &http3.Server{ Handler: mux, TLSConfig: getTLSConfig(), QUICConfig: getQuicConfig(&quic.Config{Allow0RTT: true, EnableDatagrams: true}), } addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0") Expect(err).NotTo(HaveOccurred()) conn, err := net.ListenUDP("udp", addr) Expect(err).NotTo(HaveOccurred()) port = conn.LocalAddr().(*net.UDPAddr).Port stoppedServing = make(chan struct{}) go func() { defer GinkgoRecover() server.Serve(conn) close(stoppedServing) }() }) AfterEach(func() { Expect(rt.Close()).NotTo(HaveOccurred()) Expect(server.Close()).NotTo(HaveOccurred()) Eventually(stoppedServing).Should(BeClosed()) }) BeforeEach(func() { rt = &http3.RoundTripper{ TLSClientConfig: getTLSClientConfigWithoutServerName(), QUICConfig: getQuicConfig(&quic.Config{ MaxIdleTimeout: 10 * time.Second, }), DisableCompression: true, } client = &http.Client{Transport: rt} }) It("downloads a hello", func() { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal("Hello, World!\n")) }) It("sets content-length for small response", func() { mux.HandleFunc("/small", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Write([]byte("foo")) w.Write([]byte("bar")) }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/small", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Expect(resp.Header.Get("Content-Length")).To(Equal("6")) }) It("re-establishes a QUIC connection after a dial error", func() { var dialCounter int testErr := errors.New("test error") cl := http.Client{ Transport: &http3.RoundTripper{ TLSClientConfig: getTLSClientConfig(), Dial: func(ctx context.Context, addr string, tlsConf *tls.Config, conf *quic.Config) (quic.EarlyConnection, error) { dialCounter++ if dialCounter == 1 { // make the first dial fail return nil, testErr } return quic.DialAddrEarly(ctx, addr, tlsConf, conf) }, }, } defer cl.Transport.(io.Closer).Close() _, err := cl.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).To(MatchError(testErr)) resp, err := cl.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) }) It("detects stream errors when server panics when writing response", func() { respChan := make(chan struct{}) mux.HandleFunc("/writing_and_panicking", func(w http.ResponseWriter, r *http.Request) { // no recover here as it will interfere with the handler w.Write([]byte("foobar")) w.(http.Flusher).Flush() // wait for the client to receive the response <-respChan panic(http.ErrAbortHandler) }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/writing_and_panicking", port)) close(respChan) Expect(err).ToNot(HaveOccurred()) body, err := io.ReadAll(resp.Body) Expect(err).To(HaveOccurred()) // the body will be a prefix of what's written Expect(bytes.HasPrefix([]byte("foobar"), body)).To(BeTrue()) }) It("requests to different servers with the same udpconn", func() { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remoteAddr", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) addr1 := resp.Header.Get("X-RemoteAddr") Expect(addr1).ToNot(Equal("")) resp, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d/remoteAddr", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) addr2 := resp.Header.Get("X-RemoteAddr") Expect(addr2).ToNot(Equal("")) Expect(addr1).To(Equal(addr2)) }) It("downloads concurrently", func() { group, ctx := errgroup.WithContext(context.Background()) for i := 0; i < 2; i++ { group.Go(func() error { defer GinkgoRecover() req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/hello", port), nil) Expect(err).ToNot(HaveOccurred()) resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal("Hello, World!\n")) return nil }) } err := group.Wait() Expect(err).ToNot(HaveOccurred()) }) It("sets and gets request headers", func() { handlerCalled := make(chan struct{}) mux.HandleFunc("/headers/request", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() Expect(r.Header.Get("foo")).To(Equal("bar")) Expect(r.Header.Get("lorem")).To(Equal("ipsum")) close(handlerCalled) }) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/headers/request", port), nil) Expect(err).ToNot(HaveOccurred()) req.Header.Set("foo", "bar") req.Header.Set("lorem", "ipsum") resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Eventually(handlerCalled).Should(BeClosed()) }) It("sets and gets response headers", func() { mux.HandleFunc("/headers/response", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Header().Set("foo", "bar") w.Header().Set("lorem", "ipsum") }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/headers/response", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Expect(resp.Header.Get("foo")).To(Equal("bar")) Expect(resp.Header.Get("lorem")).To(Equal("ipsum")) }) It("downloads a small file", func() { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(body).To(Equal(PRData)) }) It("downloads a large file", func() { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdatalong", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 20*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(body).To(Equal(PRDataLong)) }) It("downloads many hellos", func() { const num = 150 for i := 0; i < num; i++ { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal("Hello, World!\n")) } }) It("downloads many files, if the response is not read", func() { const num = 150 for i := 0; i < num; i++ { resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Expect(resp.Body.Close()).To(Succeed()) } }) It("posts a small message", func() { resp, err := client.Post( fmt.Sprintf("https://localhost:%d/echo", port), "text/plain", bytes.NewReader([]byte("Hello, world!")), ) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(body).To(Equal([]byte("Hello, world!"))) }) It("uploads a file", func() { resp, err := client.Post( fmt.Sprintf("https://localhost:%d/echo", port), "text/plain", bytes.NewReader(PRData), ) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(body).To(Equal(PRData)) }) It("uses gzip compression", func() { mux.HandleFunc("/gzipped/hello", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() Expect(r.Header.Get("Accept-Encoding")).To(Equal("gzip")) w.Header().Set("Content-Encoding", "gzip") w.Header().Set("foo", "bar") gw := gzip.NewWriter(w) defer gw.Close() gw.Write([]byte("Hello, World!\n")) }) client.Transport.(*http3.RoundTripper).DisableCompression = false resp, err := client.Get(fmt.Sprintf("https://localhost:%d/gzipped/hello", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Expect(resp.Uncompressed).To(BeTrue()) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal("Hello, World!\n")) }) It("handles context cancellations", func() { mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) { <-r.Context().Done() }) ctx, cancel := context.WithCancel(context.Background()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/cancel", port), nil) Expect(err).ToNot(HaveOccurred()) time.AfterFunc(50*time.Millisecond, cancel) _, err = client.Do(req) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(context.Canceled)) }) It("cancels requests", func() { handlerCalled := make(chan struct{}) mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() defer close(handlerCalled) // TODO(4508): check for request context cancellations for { if _, err := w.Write([]byte("foobar")); err != nil { var http3Err *http3.Error Expect(errors.As(err, &http3Err)).To(BeTrue()) Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c))) Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED")) return } } }) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/cancel", port), nil) Expect(err).ToNot(HaveOccurred()) ctx, cancel := context.WithCancel(context.Background()) req = req.WithContext(ctx) resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) cancel() Eventually(handlerCalled).Should(BeClosed()) _, err = resp.Body.Read([]byte{0}) var http3Err *http3.Error Expect(errors.As(err, &http3Err)).To(BeTrue()) Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c))) Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED (local)")) }) It("allows streamed HTTP requests", func() { done := make(chan struct{}) mux.HandleFunc("/echoline", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() defer close(done) w.WriteHeader(200) w.(http.Flusher).Flush() reader := bufio.NewReader(r.Body) for { msg, err := reader.ReadString('\n') if err != nil { return } _, err = w.Write([]byte(msg)) Expect(err).ToNot(HaveOccurred()) w.(http.Flusher).Flush() } }) r, w := io.Pipe() req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("https://localhost:%d/echoline", port), r) Expect(err).ToNot(HaveOccurred()) rsp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(rsp.StatusCode).To(Equal(200)) reader := bufio.NewReader(rsp.Body) for i := 0; i < 5; i++ { msg := fmt.Sprintf("Hello world, %d!\n", i) fmt.Fprint(w, msg) msgRcvd, err := reader.ReadString('\n') Expect(err).ToNot(HaveOccurred()) Expect(msgRcvd).To(Equal(msg)) } Expect(req.Body.Close()).To(Succeed()) Eventually(done).Should(BeClosed()) }) It("allows taking over the stream", func() { handlerCalled := make(chan struct{}) mux.HandleFunc("/httpstreamer", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() close(handlerCalled) w.WriteHeader(http.StatusOK) str := w.(http3.HTTPStreamer).HTTPStream() str.Write([]byte("foobar")) // Do this in a Go routine, so that the handler returns early. // This way, we can also check that the HTTP/3 doesn't close the stream. go func() { defer GinkgoRecover() _, err := io.Copy(str, str) Expect(err).ToNot(HaveOccurred()) Expect(str.Close()).To(Succeed()) }() }) req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/httpstreamer", port), nil) Expect(err).ToNot(HaveOccurred()) tlsConf := getTLSClientConfigWithoutServerName() tlsConf.NextProtos = []string{http3.NextProtoH3} conn, err := quic.DialAddr( context.Background(), fmt.Sprintf("localhost:%d", port), tlsConf, getQuicConfig(nil), ) Expect(err).ToNot(HaveOccurred()) defer conn.CloseWithError(0, "") rt := http3.SingleDestinationRoundTripper{Connection: conn} str, err := rt.OpenRequestStream(context.Background()) Expect(err).ToNot(HaveOccurred()) Expect(str.SendRequestHeader(req)).To(Succeed()) // make sure the request is received (and not stuck in some buffer, for example) Eventually(handlerCalled).Should(BeClosed()) rsp, err := str.ReadResponse() Expect(err).ToNot(HaveOccurred()) Expect(rsp.StatusCode).To(Equal(200)) b := make([]byte, 6) _, err = io.ReadFull(str, b) Expect(err).ToNot(HaveOccurred()) Expect(b).To(Equal([]byte("foobar"))) data := GeneratePRData(8 * 1024) _, err = str.Write(data) Expect(err).ToNot(HaveOccurred()) Expect(str.Close()).To(Succeed()) repl, err := io.ReadAll(str) Expect(err).ToNot(HaveOccurred()) Expect(repl).To(Equal(data)) }) It("serves QUIC connections", func() { tlsConf := getTLSConfig() tlsConf.NextProtos = []string{http3.NextProtoH3} ln, err := quic.ListenAddr("localhost:0", tlsConf, getQuicConfig(nil)) Expect(err).ToNot(HaveOccurred()) defer ln.Close() done := make(chan struct{}) go func() { defer GinkgoRecover() defer close(done) conn, err := ln.Accept(context.Background()) Expect(err).ToNot(HaveOccurred()) server.ServeQUICConn(conn) // returns once the client closes }() resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", ln.Addr().(*net.UDPAddr).Port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) client.Transport.(io.Closer).Close() Eventually(done).Should(BeClosed()) }) It("supports read deadlines", func() { mux.HandleFunc("/read-deadline", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() rc := http.NewResponseController(w) Expect(rc.SetReadDeadline(time.Now().Add(deadlineDelay))).To(Succeed()) body, err := io.ReadAll(r.Body) Expect(err).To(MatchError(os.ErrDeadlineExceeded)) Expect(body).To(ContainSubstring("aa")) w.Write([]byte("ok")) }) expectedEnd := time.Now().Add(deadlineDelay) resp, err := client.Post( fmt.Sprintf("https://localhost:%d/read-deadline", port), "text/plain", neverEnding('a'), ) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay)) Expect(err).ToNot(HaveOccurred()) Expect(time.Now().After(expectedEnd)).To(BeTrue()) Expect(string(body)).To(Equal("ok")) }) It("supports write deadlines", func() { mux.HandleFunc("/write-deadline", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() rc := http.NewResponseController(w) Expect(rc.SetWriteDeadline(time.Now().Add(deadlineDelay))).To(Succeed()) _, err := io.Copy(w, neverEnding('a')) Expect(err).To(MatchError(os.ErrDeadlineExceeded)) }) expectedEnd := time.Now().Add(deadlineDelay) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/write-deadline", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay)) Expect(err).ToNot(HaveOccurred()) Expect(time.Now().After(expectedEnd)).To(BeTrue()) Expect(string(body)).To(ContainSubstring("aa")) }) It("sets remote address", func() { mux.HandleFunc("/remote-addr", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() _, ok := r.Context().Value(http3.RemoteAddrContextKey).(net.Addr) Expect(ok).To(BeTrue()) }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remote-addr", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) }) It("sets conn context", func() { type ctxKey int var tracingID quic.ConnectionTracingID server.ConnContext = func(ctx context.Context, c quic.Connection) context.Context { serv, ok := ctx.Value(http3.ServerContextKey).(*http3.Server) Expect(ok).To(BeTrue()) Expect(serv).To(Equal(server)) ctx = context.WithValue(ctx, ctxKey(0), "Hello") ctx = context.WithValue(ctx, ctxKey(1), c) tracingID = c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID) return ctx } mux.HandleFunc("/http3-conn-context", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() v, ok := r.Context().Value(ctxKey(0)).(string) Expect(ok).To(BeTrue()) Expect(v).To(Equal("Hello")) c, ok := r.Context().Value(ctxKey(1)).(quic.Connection) Expect(ok).To(BeTrue()) Expect(c).ToNot(BeNil()) serv, ok := r.Context().Value(http3.ServerContextKey).(*http3.Server) Expect(ok).To(BeTrue()) Expect(serv).To(Equal(server)) id, ok := r.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID) Expect(ok).To(BeTrue()) Expect(id).To(Equal(tracingID)) }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/http3-conn-context", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) }) It("uses the QUIC connection context", func() { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) Expect(err).ToNot(HaveOccurred()) defer conn.Close() tr := &quic.Transport{ Conn: conn, ConnContext: func(ctx context.Context) context.Context { //nolint:staticcheck return context.WithValue(ctx, "foo", "bar") }, } defer tr.Close() tlsConf := getTLSConfig() tlsConf.NextProtos = []string{http3.NextProtoH3} ln, err := tr.Listen(tlsConf, getQuicConfig(nil)) Expect(err).ToNot(HaveOccurred()) defer ln.Close() mux.HandleFunc("/quic-conn-context", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() v, ok := r.Context().Value("foo").(string) Expect(ok).To(BeTrue()) Expect(v).To(Equal("bar")) }) go func() { defer GinkgoRecover() c, err := ln.Accept(context.Background()) Expect(err).ToNot(HaveOccurred()) server.ServeQUICConn(c) }() resp, err := client.Get(fmt.Sprintf("https://localhost:%d/quic-conn-context", conn.LocalAddr().(*net.UDPAddr).Port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) }) It("checks the server's settings", func() { tlsConf := tlsClientConfigWithoutServerName.Clone() tlsConf.NextProtos = []string{http3.NextProtoH3} conn, err := quic.DialAddr( context.Background(), fmt.Sprintf("localhost:%d", port), tlsConf, getQuicConfig(nil), ) Expect(err).ToNot(HaveOccurred()) defer conn.CloseWithError(0, "") rt := http3.SingleDestinationRoundTripper{Connection: conn} hconn := rt.Start() Eventually(hconn.ReceivedSettings(), 5*time.Second, 10*time.Millisecond).Should(BeClosed()) settings := hconn.Settings() Expect(settings.EnableExtendedConnect).To(BeTrue()) Expect(settings.EnableDatagrams).To(BeFalse()) Expect(settings.Other).To(BeEmpty()) }) It("receives the client's settings", func() { settingsChan := make(chan *http3.Settings, 1) mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() conn := w.(http3.Hijacker).Connection() Eventually(conn.ReceivedSettings(), 5*time.Second, 10*time.Millisecond).Should(BeClosed()) settingsChan <- conn.Settings() w.WriteHeader(http.StatusOK) }) rt = &http3.RoundTripper{ TLSClientConfig: getTLSClientConfigWithoutServerName(), QUICConfig: getQuicConfig(&quic.Config{ MaxIdleTimeout: 10 * time.Second, EnableDatagrams: true, }), EnableDatagrams: true, AdditionalSettings: map[uint64]uint64{1337: 42}, } req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/settings", port), nil) Expect(err).ToNot(HaveOccurred()) _, err = rt.RoundTrip(req) Expect(err).ToNot(HaveOccurred()) var settings *http3.Settings Expect(settingsChan).To(Receive(&settings)) Expect(settings).ToNot(BeNil()) Expect(settings.EnableDatagrams).To(BeTrue()) Expect(settings.EnableExtendedConnect).To(BeFalse()) Expect(settings.Other).To(HaveKeyWithValue(uint64(1337), uint64(42))) }) It("processes 1xx response", func() { header1 := "; rel=preload; as=style" header2 := "; rel=preload; as=script" data := "1xx-test-data" mux.HandleFunc("/103-early-data", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Header().Add("Link", header1) w.Header().Add("Link", header2) w.WriteHeader(http.StatusEarlyHints) n, err := w.Write([]byte(data)) Expect(err).NotTo(HaveOccurred()) Expect(n).To(Equal(len(data))) w.WriteHeader(http.StatusOK) }) var ( cnt int status int hdr textproto.MIMEHeader ) ctx := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { hdr = header status = code cnt++ return nil }, }) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/103-early-data", port), nil) Expect(err).ToNot(HaveOccurred()) resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) body, err := io.ReadAll(resp.Body) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal(data)) Expect(status).To(Equal(http.StatusEarlyHints)) Expect(hdr).To(HaveKeyWithValue("Link", []string{header1, header2})) Expect(cnt).To(Equal(1)) Expect(resp.Header).To(HaveKeyWithValue("Link", []string{header1, header2})) Expect(resp.Body.Close()).To(Succeed()) }) It("processes 1xx terminal response", func() { mux.HandleFunc("/101-switch-protocols", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Header().Add("Connection", "upgrade") w.Header().Add("Upgrade", "proto") w.WriteHeader(http.StatusSwitchingProtocols) }) var ( cnt int status int ) ctx := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { status = code cnt++ return nil }, }) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/101-switch-protocols", port), nil) Expect(err).ToNot(HaveOccurred()) resp, err := client.Do(req) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusSwitchingProtocols)) Expect(resp.Header).To(HaveKeyWithValue("Connection", []string{"upgrade"})) Expect(resp.Header).To(HaveKeyWithValue("Upgrade", []string{"proto"})) Expect(status).To(Equal(0)) Expect(cnt).To(Equal(0)) }) Context("HTTP datagrams", func() { openDatagramStream := func(h string) (_ http3.RequestStream, closeFn func()) { tlsConf := getTLSClientConfigWithoutServerName() tlsConf.NextProtos = []string{http3.NextProtoH3} conn, err := quic.DialAddr( context.Background(), fmt.Sprintf("localhost:%d", port), tlsConf, getQuicConfig(&quic.Config{EnableDatagrams: true}), ) Expect(err).ToNot(HaveOccurred()) rt := &http3.SingleDestinationRoundTripper{ Connection: conn, EnableDatagrams: true, } str, err := rt.OpenRequestStream(context.Background()) Expect(err).ToNot(HaveOccurred()) u, err := url.Parse(h) Expect(err).ToNot(HaveOccurred()) req := &http.Request{ Method: http.MethodConnect, Proto: "datagrams", Host: u.Host, URL: u, } Expect(str.SendRequestHeader(req)).To(Succeed()) rsp, err := str.ReadResponse() Expect(err).ToNot(HaveOccurred()) Expect(rsp.StatusCode).To(Equal(http.StatusOK)) return str, func() { conn.CloseWithError(0, "") } } It("sends an receives HTTP datagrams", func() { errChan := make(chan error, 1) const num = 5 datagramChan := make(chan struct{}, num) mux.HandleFunc("/datagrams", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() Expect(r.Method).To(Equal(http.MethodConnect)) conn := w.(http3.Hijacker).Connection() Eventually(conn.ReceivedSettings()).Should(BeClosed()) Expect(conn.Settings().EnableDatagrams).To(BeTrue()) w.WriteHeader(http.StatusOK) str := w.(http3.HTTPStreamer).HTTPStream() go str.Read([]byte{0}) // need to continue reading from stream to observe state transitions for { if _, err := str.ReceiveDatagram(context.Background()); err != nil { errChan <- err return } datagramChan <- struct{}{} } }) str, closeFn := openDatagramStream(fmt.Sprintf("https://localhost:%d/datagrams", port)) defer closeFn() for i := 0; i < num; i++ { b := make([]byte, 8) binary.BigEndian.PutUint64(b, uint64(i)) Expect(str.SendDatagram(bytes.Repeat(b, 100))).To(Succeed()) } var count int loop: for { select { case <-datagramChan: count++ if count >= num*4/5 { break loop } case err := <-errChan: Fail(fmt.Sprintf("receiving datagrams failed: %s", err)) } } str.CancelWrite(42) var resetErr error Eventually(errChan).Should(Receive(&resetErr)) Expect(resetErr.(*quic.StreamError).ErrorCode).To(BeEquivalentTo(42)) }) It("closes the send direction", func() { errChan := make(chan error, 1) datagramChan := make(chan []byte, 1) mux.HandleFunc("/datagrams", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() conn := w.(http3.Hijacker).Connection() Eventually(conn.ReceivedSettings()).Should(BeClosed()) Expect(conn.Settings().EnableDatagrams).To(BeTrue()) w.WriteHeader(http.StatusOK) str := w.(http3.HTTPStreamer).HTTPStream() go str.Read([]byte{0}) // need to continue reading from stream to observe state transitions for { data, err := str.ReceiveDatagram(context.Background()) if err != nil { errChan <- err return } datagramChan <- data } }) str, closeFn := openDatagramStream(fmt.Sprintf("https://localhost:%d/datagrams", port)) defer closeFn() go str.Read([]byte{0}) Expect(str.SendDatagram([]byte("foo"))).To(Succeed()) Eventually(datagramChan).Should(Receive(Equal([]byte("foo")))) // signal that we're done sending str.Close() var resetErr error Eventually(errChan).Should(Receive(&resetErr)) Expect(resetErr).To(Equal(io.EOF)) // make sure we can't send anymore Expect(str.SendDatagram([]byte("foo"))).ToNot(Succeed()) }) It("detecting a stream reset from the server", func() { errChan := make(chan error, 1) datagramChan := make(chan []byte, 1) mux.HandleFunc("/datagrams", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() conn := w.(http3.Hijacker).Connection() Eventually(conn.ReceivedSettings()).Should(BeClosed()) Expect(conn.Settings().EnableDatagrams).To(BeTrue()) w.WriteHeader(http.StatusOK) str := w.(http3.HTTPStreamer).HTTPStream() go str.Read([]byte{0}) // need to continue reading from stream to observe state transitions for { data, err := str.ReceiveDatagram(context.Background()) if err != nil { errChan <- err return } str.CancelRead(42) datagramChan <- data } }) str, closeFn := openDatagramStream(fmt.Sprintf("https://localhost:%d/datagrams", port)) defer closeFn() go str.Read([]byte{0}) Expect(str.SendDatagram([]byte("foo"))).To(Succeed()) Eventually(datagramChan).Should(Receive(Equal([]byte("foo")))) // signal that we're done sending var resetErr error Eventually(errChan).Should(Receive(&resetErr)) Expect(resetErr).To(Equal(&quic.StreamError{ErrorCode: 42, Remote: false})) // make sure we can't send anymore Expect(str.SendDatagram([]byte("foo"))).To(Equal(&quic.StreamError{ErrorCode: 42, Remote: true})) }) }) Context("0-RTT", func() { runCountingProxy := func(serverPort int, rtt time.Duration) (*quicproxy.QuicProxy, *atomic.Uint32) { var num0RTTPackets atomic.Uint32 proxy, err := quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{ RemoteAddr: fmt.Sprintf("localhost:%d", serverPort), DelayPacket: func(_ quicproxy.Direction, data []byte) time.Duration { if contains0RTTPacket(data) { num0RTTPackets.Add(1) } return rtt / 2 }, }) Expect(err).ToNot(HaveOccurred()) return proxy, &num0RTTPackets } It("sends 0-RTT GET requests", func() { proxy, num0RTTPackets := runCountingProxy(port, scaleDuration(50*time.Millisecond)) defer proxy.Close() tlsConf := getTLSClientConfigWithoutServerName() puts := make(chan string, 10) tlsConf.ClientSessionCache = newClientSessionCache(tls.NewLRUClientSessionCache(10), nil, puts) rt := &http3.RoundTripper{ TLSClientConfig: tlsConf, QUICConfig: getQuicConfig(&quic.Config{ MaxIdleTimeout: 10 * time.Second, }), DisableCompression: true, } defer rt.Close() mux.HandleFunc("/0rtt", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(strconv.FormatBool(!r.TLS.HandshakeComplete))) }) req, err := http.NewRequest(http3.MethodGet0RTT, fmt.Sprintf("https://localhost:%d/0rtt", proxy.LocalPort()), nil) Expect(err).ToNot(HaveOccurred()) rsp, err := rt.RoundTrip(req) Expect(err).ToNot(HaveOccurred()) Expect(rsp.StatusCode).To(BeEquivalentTo(200)) data, err := io.ReadAll(rsp.Body) Expect(err).ToNot(HaveOccurred()) Expect(string(data)).To(Equal("false")) Expect(num0RTTPackets.Load()).To(BeZero()) Eventually(puts).Should(Receive()) rt2 := &http3.RoundTripper{ TLSClientConfig: rt.TLSClientConfig, QUICConfig: rt.QUICConfig, DisableCompression: true, } defer rt2.Close() rsp, err = rt2.RoundTrip(req) Expect(err).ToNot(HaveOccurred()) Expect(rsp.StatusCode).To(BeEquivalentTo(200)) data, err = io.ReadAll(rsp.Body) Expect(err).ToNot(HaveOccurred()) Expect(string(data)).To(Equal("true")) Expect(num0RTTPackets.Load()).To(BeNumerically(">", 0)) }) }) })