blob: 2b6de06d50900251a295e88b18af801e644494ab [file]
// 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.
//go:build go1.25
package http3
import (
"io"
"maps"
"net/http"
"net/netip"
"net/url"
"reflect"
"testing"
"testing/synctest"
"time"
"golang.org/x/net/internal/quic/quicwire"
"golang.org/x/net/quic"
)
// requestHeader is a helper function to make sure that all required
// pseudo-headers exist in an http.Header used for a request. Per
// https://www.rfc-editor.org/rfc/rfc9114.html#name-request-pseudo-header-field:
// "All HTTP/3 requests MUST include exactly one value for the :method,
// :scheme, and :path pseudo-header fields, unless the request is a CONNECT
// request;"
func requestHeader(h http.Header) http.Header {
minimalHeader := http.Header{
":method": {"GET"},
":scheme": {"https"},
":path": {"/"},
}
maps.Copy(minimalHeader, h)
return minimalHeader
}
func TestServerReceivePushStream(t *testing.T) {
// "[...] if a server receives a client-initiated push stream,
// this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR."
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.2-3
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, nil)
tc := ts.connect()
tc.newStream(streamTypePush)
tc.wantClosed("invalid client-created push stream", errH3StreamCreationError)
})
}
func TestServerCancelPushForUnsentPromise(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, nil)
tc := ts.connect()
tc.greet()
const pushID = 100
tc.control.writeVarint(int64(frameTypeCancelPush))
tc.control.writeVarint(int64(quicwire.SizeVarint(pushID)))
tc.control.writeVarint(pushID)
tc.control.Flush()
tc.wantClosed("client canceled never-sent push ID", errH3IDError)
})
}
func TestServerHeader(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := w.Header()
for key, values := range r.Header {
for _, value := range values {
header.Add(key, value)
}
}
w.WriteHeader(204)
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(http.Header{
"header-from-client": {"that", "should", "be", "echoed"},
}))
synctest.Wait()
reqStream.wantHeaders(http.Header{
":status": {"204"},
"Header-From-Client": {"that", "should", "be", "echoed"},
})
reqStream.wantClosed("request is complete")
})
}
func TestServerPseudoHeader(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pseudo-headers from client request should populate a specific
// field in http.Request, and should not be part of http.Request.Header.
if len(r.Header) != 0 {
t.Errorf("got %v, want request header to be empty", r.Header)
}
if r.Method != "GET" {
t.Errorf("got %v, want GET method", r.Method)
}
if r.Host != "fake.tld:1234" {
t.Errorf("got %v, want fake.tld:1234", r.Host)
}
wantURL := &url.URL{
Path: "/some/path",
RawQuery: "query=value&query2=value2#fragment",
}
if !reflect.DeepEqual(r.URL, wantURL) {
t.Errorf("got %v, want URL to be %v", r.URL, wantURL)
}
// Conversely, server should not be able to set pseudo-headers by
// writing to the ResponseWriter's Header.
header := w.Header()
header.Add(":status", "123")
w.WriteHeader(321)
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(http.Header{
":method": {"GET"},
":authority": {"fake.tld:1234"},
":scheme": {"https"},
":path": {"/some/path?query=value&query2=value2#fragment"},
})
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"321"}})
reqStream.wantClosed("request is complete")
reqStream = tc.newStream(streamTypeRequest)
reqStream.writeHeaders(http.Header{}) // Missing pseudo-header.
synctest.Wait()
reqStream.wantError(quic.StreamErrorCode(errH3MessageError))
})
}
func TestServerInvalidHeader(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("valid-name", "valid value")
// Invalid headers are skipped.
w.Header().Add("invalid name with spaces", "some value")
w.Header().Add("some-name", "invalid value with \n")
w.Header().Add("valid-name-2", "valid value 2")
w.WriteHeader(200)
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
synctest.Wait()
reqStream.wantHeaders(http.Header{
":status": {"200"},
"Valid-Name": {"valid value"},
"Valid-Name-2": {"valid value 2"},
})
reqStream.wantClosed("request is complete")
})
}
func TestServerBody(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
w.Write([]byte(r.URL.Path)) // Implicitly calls w.WriteHeader(200).
w.Write(body)
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
bodyContent := []byte("some body content that should be echoed")
reqStream.writeData(bodyContent)
reqStream.stream.stream.CloseWrite()
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"200"}})
reqStream.wantData([]byte("/"))
reqStream.wantData(bodyContent)
reqStream.wantClosed("request is complete")
})
}
func TestServerHeadResponseNoBody(t *testing.T) {
bodyContent := []byte("response body that will not be sent for HEAD requests")
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(bodyContent)
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"200"}})
reqStream.wantData(bodyContent)
reqStream.wantClosed("request is complete")
reqStream = tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(http.Header{":method": {http.MethodHead}}))
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"200"}})
reqStream.wantClosed("request is complete")
})
}
func TestServerHandlerEmpty(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Empty handler should return a 200 OK
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"200"}})
reqStream.wantClosed("request is complete")
})
}
func TestServerHandlerFlushing(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second)
w.Write([]byte("first"))
time.Sleep(time.Second)
w.Write([]byte("second"))
w.(http.Flusher).Flush()
time.Sleep(time.Second)
w.Write([]byte("third"))
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
synctest.Wait()
respBody := make([]byte, 100)
time.Sleep(time.Second)
synctest.Wait()
if n, err := reqStream.Read(respBody); err == nil {
t.Errorf("got %v bytes read, want no message yet", n)
}
time.Sleep(time.Second)
synctest.Wait()
if _, err := reqStream.Read(respBody); err != nil {
t.Errorf("failed to read partial response from server, got err: %v", err)
}
time.Sleep(time.Second)
synctest.Wait()
if _, err := reqStream.Read(respBody); err != io.EOF {
t.Errorf("got err %v, want EOF", err)
}
reqStream.wantClosed("request is complete")
})
}
func TestServerHandlerStreaming(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
stream := make(chan string)
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Flushing when we have not written anything yet implicitly calls
// w.WriteHeader(200).
w.(http.Flusher).Flush()
for str := range stream {
w.Write([]byte(str))
w.(http.Flusher).Flush()
}
}))
tc := ts.connect()
tc.greet()
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(nil))
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"200"}})
for _, data := range []string{"a", "bunch", "of", "things", "to", "stream"} {
stream <- data
reqStream.wantData([]byte(data))
}
close(stream)
reqStream.wantClosed("request is complete")
})
}
func TestServerExpect100Continue(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
streamIdle := make(chan bool)
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Expect: 100-continue header should not be accessible from the
// server handler.
if len(r.Header) > 0 {
t.Errorf("got %v, want request header to be empty", r.Header)
}
// Reading the body will cause the server to call w.WriteHeader(100).
<-streamIdle
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
// Implicitly calls w.WriteHeader(200) since non-1XX status code
// has been sent yet so far.
w.Write(body)
}))
tc := ts.connect()
tc.greet()
// Client sends an Expect: 100-continue request.
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(http.Header{
"Expect": {"100-continue"},
}))
reqStream.wantIdle("stream is idle until server sends an HTTP 100 status")
streamIdle <- true
// Wait until server responds with HTTP status 100 before sending the
// body.
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"100"}})
body := []byte("body that will be echoed back if we get status 100")
reqStream.writeData(body)
reqStream.stream.stream.CloseWrite()
// Receive the server's response after sending the body.
reqStream.wantHeaders(http.Header{":status": {"200"}})
reqStream.wantData(body)
reqStream.wantClosed("request is complete")
})
}
func TestServerExpect100ContinueRejected(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
rejectBody := []byte("not allowed")
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write(rejectBody)
}))
tc := ts.connect()
tc.greet()
// Client sends an Expect: 100-continue request.
reqStream := tc.newStream(streamTypeRequest)
reqStream.writeHeaders(requestHeader(http.Header{
"Expect": {"100-continue"},
}))
// Server rejects it.
synctest.Wait()
reqStream.wantHeaders(http.Header{":status": {"403"}})
reqStream.wantData(rejectBody)
reqStream.wantClosed("request is complete")
})
}
type testServer struct {
t testing.TB
s *Server
tn testNet
*testQUICEndpoint
addr netip.AddrPort
}
type testQUICEndpoint struct {
t testing.TB
e *quic.Endpoint
}
type testServerConn struct {
ts *testServer
*testQUICConn
control *testQUICStream
}
func newTestServer(t testing.TB, handler http.Handler) *testServer {
t.Helper()
ts := &testServer{
t: t,
s: &Server{
Config: &quic.Config{
TLSConfig: testTLSConfig,
},
Handler: handler,
},
}
e := ts.tn.newQUICEndpoint(t, ts.s.Config)
ts.addr = e.LocalAddr()
go ts.s.Serve(e)
return ts
}
func (ts *testServer) connect() *testServerConn {
ts.t.Helper()
config := &quic.Config{TLSConfig: testTLSConfig}
e := ts.tn.newQUICEndpoint(ts.t, nil)
qconn, err := e.Dial(ts.t.Context(), "udp", ts.addr.String(), config)
if err != nil {
ts.t.Fatal(err)
}
tc := &testServerConn{
ts: ts,
testQUICConn: newTestQUICConn(ts.t, qconn),
}
synctest.Wait()
return tc
}
// greet performs initial connection handshaking with the server.
func (tc *testServerConn) greet() {
// Client creates a control stream.
tc.control = tc.newStream(streamTypeControl)
tc.control.writeVarint(int64(frameTypeSettings))
tc.control.writeVarint(0) // size
tc.control.Flush()
synctest.Wait()
}