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("closes the connection after idle timeout", func() { server.IdleTimeout = 100 * time.Millisecond _, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).ToNot(HaveOccurred()) time.Sleep(150 * time.Millisecond) _, err = client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) Expect(err).ToNot(MatchError("idle timeout")) server.IdleTimeout = 0 }) 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(resp.ContentLength).To(BeEquivalentTo(-1)) 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("foo", "bar") 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("Foo", []string{"bar"})) 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)) }) }) It("sends and receives trailers", func() { mux.HandleFunc("/trailers", func(w http.ResponseWriter, r *http.Request) { defer GinkgoRecover() w.Header().Set("Trailer", "AtEnd1, AtEnd2") w.Header().Add("Trailer", "Never") w.Header().Add("Trailer", "LAST") w.Header().Set("Content-Type", "text/plain; charset=utf-8") // normal header w.WriteHeader(http.StatusOK) w.Header().Set("AtEnd1", "value 1") io.WriteString(w, "This HTTP response has both headers before this text and trailers at the end.\n") w.(http.Flusher).Flush() w.Header().Set("AtEnd2", "value 2") io.WriteString(w, "More text\n") w.(http.Flusher).Flush() w.Header().Set("LAST", "value 3") w.Header().Set(http.TrailerPrefix+"Unannounced", "Surprise!") w.Header().Set("Late-Header", "No surprise!") }) resp, err := client.Get(fmt.Sprintf("https://localhost:%d/trailers", port)) Expect(err).ToNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(200)) Expect(resp.Header.Get("Trailer")).To(Equal("")) Expect(resp.Header).To(Not(HaveKey("Atend1"))) Expect(resp.Header).To(Not(HaveKey("Atend2"))) Expect(resp.Header).To(Not(HaveKey("Never"))) Expect(resp.Header).To(Not(HaveKey("Last"))) Expect(resp.Header).To(Not(HaveKey("Late-Header"))) Expect(resp.Trailer).To(Equal(http.Header(map[string][]string{ "Atend1": nil, "Atend2": nil, "Never": nil, "Last": nil, }))) body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) Expect(err).ToNot(HaveOccurred()) Expect(string(body)).To(Equal("This HTTP response has both headers before this text and trailers at the end.\nMore text\n")) for k := range resp.Header { Expect(k).To(Not(HavePrefix(http.TrailerPrefix))) } Expect(resp.Trailer).To(Equal(http.Header(map[string][]string{ "Atend1": {"value 1"}, "Atend2": {"value 2"}, "Last": {"value 3"}, "Unannounced": {"Surprise!"}, }))) }) })