github.com/MerlinKodo/quic-go@v0.39.2/integrationtests/self/http_test.go (about)

     1  package self_test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"strconv"
    15  	"time"
    16  
    17  	"golang.org/x/sync/errgroup"
    18  
    19  	"github.com/MerlinKodo/quic-go"
    20  	"github.com/MerlinKodo/quic-go/http3"
    21  
    22  	. "github.com/onsi/ginkgo/v2"
    23  	. "github.com/onsi/gomega"
    24  	"github.com/onsi/gomega/gbytes"
    25  )
    26  
    27  type neverEnding byte
    28  
    29  func (b neverEnding) Read(p []byte) (n int, err error) {
    30  	for i := range p {
    31  		p[i] = byte(b)
    32  	}
    33  	return len(p), nil
    34  }
    35  
    36  const deadlineDelay = 250 * time.Millisecond
    37  
    38  var _ = Describe("HTTP tests", func() {
    39  	var (
    40  		mux            *http.ServeMux
    41  		client         *http.Client
    42  		rt             *http3.RoundTripper
    43  		server         *http3.Server
    44  		stoppedServing chan struct{}
    45  		port           int
    46  	)
    47  
    48  	BeforeEach(func() {
    49  		mux = http.NewServeMux()
    50  		mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    51  			defer GinkgoRecover()
    52  			io.WriteString(w, "Hello, World!\n") // don't check the error here. Stream may be reset.
    53  		})
    54  
    55  		mux.HandleFunc("/prdata", func(w http.ResponseWriter, r *http.Request) {
    56  			defer GinkgoRecover()
    57  			sl := r.URL.Query().Get("len")
    58  			if sl != "" {
    59  				var err error
    60  				l, err := strconv.Atoi(sl)
    61  				Expect(err).NotTo(HaveOccurred())
    62  				w.Write(GeneratePRData(l)) // don't check the error here. Stream may be reset.
    63  			} else {
    64  				w.Write(PRData) // don't check the error here. Stream may be reset.
    65  			}
    66  		})
    67  
    68  		mux.HandleFunc("/prdatalong", func(w http.ResponseWriter, r *http.Request) {
    69  			defer GinkgoRecover()
    70  			w.Write(PRDataLong) // don't check the error here. Stream may be reset.
    71  		})
    72  
    73  		mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
    74  			defer GinkgoRecover()
    75  			body, err := io.ReadAll(r.Body)
    76  			Expect(err).NotTo(HaveOccurred())
    77  			w.Write(body) // don't check the error here. Stream may be reset.
    78  		})
    79  
    80  		mux.HandleFunc("/remoteAddr", func(w http.ResponseWriter, r *http.Request) {
    81  			defer GinkgoRecover()
    82  			w.Header().Set("X-RemoteAddr", r.RemoteAddr)
    83  			w.WriteHeader(http.StatusOK)
    84  		})
    85  
    86  		server = &http3.Server{
    87  			Handler:    mux,
    88  			TLSConfig:  getTLSConfig(),
    89  			QuicConfig: getQuicConfig(nil),
    90  		}
    91  
    92  		addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
    93  		Expect(err).NotTo(HaveOccurred())
    94  		conn, err := net.ListenUDP("udp", addr)
    95  		Expect(err).NotTo(HaveOccurred())
    96  		port = conn.LocalAddr().(*net.UDPAddr).Port
    97  
    98  		stoppedServing = make(chan struct{})
    99  
   100  		go func() {
   101  			defer GinkgoRecover()
   102  			server.Serve(conn)
   103  			close(stoppedServing)
   104  		}()
   105  	})
   106  
   107  	AfterEach(func() {
   108  		Expect(rt.Close()).NotTo(HaveOccurred())
   109  		Expect(server.Close()).NotTo(HaveOccurred())
   110  		Eventually(stoppedServing).Should(BeClosed())
   111  	})
   112  
   113  	BeforeEach(func() {
   114  		rt = &http3.RoundTripper{
   115  			TLSClientConfig:    getTLSClientConfigWithoutServerName(),
   116  			DisableCompression: true,
   117  			QuicConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
   118  		}
   119  		client = &http.Client{Transport: rt}
   120  	})
   121  
   122  	It("downloads a hello", func() {
   123  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port))
   124  		Expect(err).ToNot(HaveOccurred())
   125  		Expect(resp.StatusCode).To(Equal(200))
   126  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
   127  		Expect(err).ToNot(HaveOccurred())
   128  		Expect(string(body)).To(Equal("Hello, World!\n"))
   129  	})
   130  
   131  	It("sets content-length for small response", func() {
   132  		mux.HandleFunc("/small", func(w http.ResponseWriter, r *http.Request) {
   133  			defer GinkgoRecover()
   134  			w.Write([]byte("foobar"))
   135  		})
   136  
   137  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/small", port))
   138  		Expect(err).ToNot(HaveOccurred())
   139  		Expect(resp.StatusCode).To(Equal(200))
   140  		Expect(resp.Header.Get("Content-Length")).To(Equal(strconv.Itoa(len("foobar"))))
   141  	})
   142  
   143  	It("requests to different servers with the same udpconn", func() {
   144  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remoteAddr", port))
   145  		Expect(err).ToNot(HaveOccurred())
   146  		Expect(resp.StatusCode).To(Equal(200))
   147  		addr1 := resp.Header.Get("X-RemoteAddr")
   148  		Expect(addr1).ToNot(Equal(""))
   149  		resp, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d/remoteAddr", port))
   150  		Expect(err).ToNot(HaveOccurred())
   151  		Expect(resp.StatusCode).To(Equal(200))
   152  		addr2 := resp.Header.Get("X-RemoteAddr")
   153  		Expect(addr2).ToNot(Equal(""))
   154  		Expect(addr1).To(Equal(addr2))
   155  	})
   156  
   157  	It("downloads concurrently", func() {
   158  		group, ctx := errgroup.WithContext(context.Background())
   159  		for i := 0; i < 2; i++ {
   160  			group.Go(func() error {
   161  				req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/hello", port), nil)
   162  				Expect(err).ToNot(HaveOccurred())
   163  				resp, err := client.Do(req)
   164  				Expect(err).ToNot(HaveOccurred())
   165  				Expect(resp.StatusCode).To(Equal(200))
   166  				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
   167  				Expect(err).ToNot(HaveOccurred())
   168  				Expect(string(body)).To(Equal("Hello, World!\n"))
   169  
   170  				return nil
   171  			})
   172  		}
   173  
   174  		err := group.Wait()
   175  		Expect(err).ToNot(HaveOccurred())
   176  	})
   177  
   178  	It("sets and gets request headers", func() {
   179  		handlerCalled := make(chan struct{})
   180  		mux.HandleFunc("/headers/request", func(w http.ResponseWriter, r *http.Request) {
   181  			defer GinkgoRecover()
   182  			Expect(r.Header.Get("foo")).To(Equal("bar"))
   183  			Expect(r.Header.Get("lorem")).To(Equal("ipsum"))
   184  			close(handlerCalled)
   185  		})
   186  
   187  		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/headers/request", port), nil)
   188  		Expect(err).ToNot(HaveOccurred())
   189  		req.Header.Set("foo", "bar")
   190  		req.Header.Set("lorem", "ipsum")
   191  		resp, err := client.Do(req)
   192  		Expect(err).ToNot(HaveOccurred())
   193  		Expect(resp.StatusCode).To(Equal(200))
   194  		Eventually(handlerCalled).Should(BeClosed())
   195  	})
   196  
   197  	It("sets and gets response headers", func() {
   198  		mux.HandleFunc("/headers/response", func(w http.ResponseWriter, r *http.Request) {
   199  			defer GinkgoRecover()
   200  			w.Header().Set("foo", "bar")
   201  			w.Header().Set("lorem", "ipsum")
   202  		})
   203  
   204  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/headers/response", port))
   205  		Expect(err).ToNot(HaveOccurred())
   206  		Expect(resp.StatusCode).To(Equal(200))
   207  		Expect(resp.Header.Get("foo")).To(Equal("bar"))
   208  		Expect(resp.Header.Get("lorem")).To(Equal("ipsum"))
   209  	})
   210  
   211  	It("downloads a small file", func() {
   212  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port))
   213  		Expect(err).ToNot(HaveOccurred())
   214  		Expect(resp.StatusCode).To(Equal(200))
   215  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
   216  		Expect(err).ToNot(HaveOccurred())
   217  		Expect(body).To(Equal(PRData))
   218  	})
   219  
   220  	It("downloads a large file", func() {
   221  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdatalong", port))
   222  		Expect(err).ToNot(HaveOccurred())
   223  		Expect(resp.StatusCode).To(Equal(200))
   224  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 20*time.Second))
   225  		Expect(err).ToNot(HaveOccurred())
   226  		Expect(body).To(Equal(PRDataLong))
   227  	})
   228  
   229  	It("downloads many hellos", func() {
   230  		const num = 150
   231  
   232  		for i := 0; i < num; i++ {
   233  			resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port))
   234  			Expect(err).ToNot(HaveOccurred())
   235  			Expect(resp.StatusCode).To(Equal(200))
   236  			body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
   237  			Expect(err).ToNot(HaveOccurred())
   238  			Expect(string(body)).To(Equal("Hello, World!\n"))
   239  		}
   240  	})
   241  
   242  	It("downloads many files, if the response is not read", func() {
   243  		const num = 150
   244  
   245  		for i := 0; i < num; i++ {
   246  			resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port))
   247  			Expect(err).ToNot(HaveOccurred())
   248  			Expect(resp.StatusCode).To(Equal(200))
   249  			Expect(resp.Body.Close()).To(Succeed())
   250  		}
   251  	})
   252  
   253  	It("posts a small message", func() {
   254  		resp, err := client.Post(
   255  			fmt.Sprintf("https://localhost:%d/echo", port),
   256  			"text/plain",
   257  			bytes.NewReader([]byte("Hello, world!")),
   258  		)
   259  		Expect(err).ToNot(HaveOccurred())
   260  		Expect(resp.StatusCode).To(Equal(200))
   261  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
   262  		Expect(err).ToNot(HaveOccurred())
   263  		Expect(body).To(Equal([]byte("Hello, world!")))
   264  	})
   265  
   266  	It("uploads a file", func() {
   267  		resp, err := client.Post(
   268  			fmt.Sprintf("https://localhost:%d/echo", port),
   269  			"text/plain",
   270  			bytes.NewReader(PRData),
   271  		)
   272  		Expect(err).ToNot(HaveOccurred())
   273  		Expect(resp.StatusCode).To(Equal(200))
   274  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
   275  		Expect(err).ToNot(HaveOccurred())
   276  		Expect(body).To(Equal(PRData))
   277  	})
   278  
   279  	It("uses gzip compression", func() {
   280  		mux.HandleFunc("/gzipped/hello", func(w http.ResponseWriter, r *http.Request) {
   281  			defer GinkgoRecover()
   282  			Expect(r.Header.Get("Accept-Encoding")).To(Equal("gzip"))
   283  			w.Header().Set("Content-Encoding", "gzip")
   284  			w.Header().Set("foo", "bar")
   285  
   286  			gw := gzip.NewWriter(w)
   287  			defer gw.Close()
   288  			gw.Write([]byte("Hello, World!\n"))
   289  		})
   290  
   291  		client.Transport.(*http3.RoundTripper).DisableCompression = false
   292  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/gzipped/hello", port))
   293  		Expect(err).ToNot(HaveOccurred())
   294  		Expect(resp.StatusCode).To(Equal(200))
   295  		Expect(resp.Uncompressed).To(BeTrue())
   296  
   297  		body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
   298  		Expect(err).ToNot(HaveOccurred())
   299  		Expect(string(body)).To(Equal("Hello, World!\n"))
   300  	})
   301  
   302  	It("cancels requests", func() {
   303  		handlerCalled := make(chan struct{})
   304  		mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
   305  			defer GinkgoRecover()
   306  			defer close(handlerCalled)
   307  			for {
   308  				if _, err := w.Write([]byte("foobar")); err != nil {
   309  					Expect(r.Context().Done()).To(BeClosed())
   310  					var http3Err *http3.Error
   311  					Expect(errors.As(err, &http3Err)).To(BeTrue())
   312  					Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c)))
   313  					Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED"))
   314  					return
   315  				}
   316  			}
   317  		})
   318  
   319  		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/cancel", port), nil)
   320  		Expect(err).ToNot(HaveOccurred())
   321  		ctx, cancel := context.WithCancel(context.Background())
   322  		req = req.WithContext(ctx)
   323  		resp, err := client.Do(req)
   324  		Expect(err).ToNot(HaveOccurred())
   325  		Expect(resp.StatusCode).To(Equal(200))
   326  		cancel()
   327  		Eventually(handlerCalled).Should(BeClosed())
   328  		_, err = resp.Body.Read([]byte{0})
   329  		var http3Err *http3.Error
   330  		Expect(errors.As(err, &http3Err)).To(BeTrue())
   331  		Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c)))
   332  		Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED (local)"))
   333  	})
   334  
   335  	It("allows streamed HTTP requests", func() {
   336  		done := make(chan struct{})
   337  		mux.HandleFunc("/echoline", func(w http.ResponseWriter, r *http.Request) {
   338  			defer GinkgoRecover()
   339  			defer close(done)
   340  			w.WriteHeader(200)
   341  			w.(http.Flusher).Flush()
   342  			reader := bufio.NewReader(r.Body)
   343  			for {
   344  				msg, err := reader.ReadString('\n')
   345  				if err != nil {
   346  					return
   347  				}
   348  				_, err = w.Write([]byte(msg))
   349  				Expect(err).ToNot(HaveOccurred())
   350  				w.(http.Flusher).Flush()
   351  			}
   352  		})
   353  
   354  		r, w := io.Pipe()
   355  		req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("https://localhost:%d/echoline", port), r)
   356  		Expect(err).ToNot(HaveOccurred())
   357  		rsp, err := client.Do(req)
   358  		Expect(err).ToNot(HaveOccurred())
   359  		Expect(rsp.StatusCode).To(Equal(200))
   360  
   361  		reader := bufio.NewReader(rsp.Body)
   362  		for i := 0; i < 5; i++ {
   363  			msg := fmt.Sprintf("Hello world, %d!\n", i)
   364  			fmt.Fprint(w, msg)
   365  			msgRcvd, err := reader.ReadString('\n')
   366  			Expect(err).ToNot(HaveOccurred())
   367  			Expect(msgRcvd).To(Equal(msg))
   368  		}
   369  		Expect(req.Body.Close()).To(Succeed())
   370  		Eventually(done).Should(BeClosed())
   371  	})
   372  
   373  	It("allows taking over the stream", func() {
   374  		mux.HandleFunc("/httpstreamer", func(w http.ResponseWriter, r *http.Request) {
   375  			defer GinkgoRecover()
   376  			w.WriteHeader(200)
   377  			w.(http.Flusher).Flush()
   378  
   379  			str := r.Body.(http3.HTTPStreamer).HTTPStream()
   380  			str.Write([]byte("foobar"))
   381  
   382  			// Do this in a Go routine, so that the handler returns early.
   383  			// This way, we can also check that the HTTP/3 doesn't close the stream.
   384  			go func() {
   385  				defer GinkgoRecover()
   386  				_, err := io.Copy(str, str)
   387  				Expect(err).ToNot(HaveOccurred())
   388  				Expect(str.Close()).To(Succeed())
   389  			}()
   390  		})
   391  
   392  		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/httpstreamer", port), nil)
   393  		Expect(err).ToNot(HaveOccurred())
   394  		rsp, err := client.Transport.(*http3.RoundTripper).RoundTripOpt(req, http3.RoundTripOpt{DontCloseRequestStream: true})
   395  		Expect(err).ToNot(HaveOccurred())
   396  		Expect(rsp.StatusCode).To(Equal(200))
   397  
   398  		str := rsp.Body.(http3.HTTPStreamer).HTTPStream()
   399  		b := make([]byte, 6)
   400  		_, err = io.ReadFull(str, b)
   401  		Expect(err).ToNot(HaveOccurred())
   402  		Expect(b).To(Equal([]byte("foobar")))
   403  
   404  		data := GeneratePRData(8 * 1024)
   405  		_, err = str.Write(data)
   406  		Expect(err).ToNot(HaveOccurred())
   407  		Expect(str.Close()).To(Succeed())
   408  		repl, err := io.ReadAll(str)
   409  		Expect(err).ToNot(HaveOccurred())
   410  		Expect(repl).To(Equal(data))
   411  	})
   412  
   413  	It("serves other QUIC connections", func() {
   414  		tlsConf := getTLSConfig()
   415  		tlsConf.NextProtos = []string{http3.NextProtoH3}
   416  		ln, err := quic.ListenAddr("localhost:0", tlsConf, nil)
   417  		Expect(err).ToNot(HaveOccurred())
   418  		defer ln.Close()
   419  		done := make(chan struct{})
   420  		go func() {
   421  			defer GinkgoRecover()
   422  			defer close(done)
   423  			conn, err := ln.Accept(context.Background())
   424  			Expect(err).ToNot(HaveOccurred())
   425  			Expect(server.ServeQUICConn(conn)).To(Succeed())
   426  		}()
   427  
   428  		resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", ln.Addr().(*net.UDPAddr).Port))
   429  		Expect(err).ToNot(HaveOccurred())
   430  		Expect(resp.StatusCode).To(Equal(http.StatusOK))
   431  		client.Transport.(io.Closer).Close()
   432  		Eventually(done).Should(BeClosed())
   433  	})
   434  
   435  	if go120 {
   436  		It("supports read deadlines", func() {
   437  			mux.HandleFunc("/read-deadline", func(w http.ResponseWriter, r *http.Request) {
   438  				defer GinkgoRecover()
   439  				err := setReadDeadline(w, time.Now().Add(deadlineDelay))
   440  				Expect(err).ToNot(HaveOccurred())
   441  
   442  				body, err := io.ReadAll(r.Body)
   443  				Expect(err).To(MatchError(os.ErrDeadlineExceeded))
   444  				Expect(body).To(ContainSubstring("aa"))
   445  
   446  				w.Write([]byte("ok"))
   447  			})
   448  
   449  			expectedEnd := time.Now().Add(deadlineDelay)
   450  			resp, err := client.Post(
   451  				fmt.Sprintf("https://localhost:%d/read-deadline", port),
   452  				"text/plain",
   453  				neverEnding('a'),
   454  			)
   455  			Expect(err).ToNot(HaveOccurred())
   456  			Expect(resp.StatusCode).To(Equal(200))
   457  
   458  			body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
   459  			Expect(err).ToNot(HaveOccurred())
   460  			Expect(time.Now().After(expectedEnd)).To(BeTrue())
   461  			Expect(string(body)).To(Equal("ok"))
   462  		})
   463  
   464  		It("supports write deadlines", func() {
   465  			mux.HandleFunc("/write-deadline", func(w http.ResponseWriter, r *http.Request) {
   466  				defer GinkgoRecover()
   467  				err := setWriteDeadline(w, time.Now().Add(deadlineDelay))
   468  				Expect(err).ToNot(HaveOccurred())
   469  
   470  				_, err = io.Copy(w, neverEnding('a'))
   471  				Expect(err).To(MatchError(os.ErrDeadlineExceeded))
   472  			})
   473  
   474  			expectedEnd := time.Now().Add(deadlineDelay)
   475  
   476  			resp, err := client.Get(fmt.Sprintf("https://localhost:%d/write-deadline", port))
   477  			Expect(err).ToNot(HaveOccurred())
   478  			Expect(resp.StatusCode).To(Equal(200))
   479  
   480  			body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay))
   481  			Expect(err).ToNot(HaveOccurred())
   482  			Expect(time.Now().After(expectedEnd)).To(BeTrue())
   483  			Expect(string(body)).To(ContainSubstring("aa"))
   484  		})
   485  	}
   486  })