1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
package self_test
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httptrace"
"net/textproto"
"testing"
"time"
"github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/require"
)
func TestHTTPClientTrace(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/client-trace", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond)
w.WriteHeader(http.StatusContinue)
})
port := startHTTPServer(t, mux)
buf := make([]byte, 1)
type event struct {
Key string
Args any
}
eventQueue := make(chan event, 100)
wait100Continue := false
trace := httptrace.ClientTrace{
GetConn: func(hostPort string) { eventQueue <- event{Key: "GetConn", Args: hostPort} },
GotConn: func(info httptrace.GotConnInfo) { eventQueue <- event{Key: "GotConn", Args: info} },
GotFirstResponseByte: func() { eventQueue <- event{Key: "GotFirstResponseByte"} },
Got100Continue: func() { eventQueue <- event{Key: "Got100Continue"} },
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
eventQueue <- event{Key: "Got1xxResponse", Args: code}
return nil
},
DNSStart: func(di httptrace.DNSStartInfo) { eventQueue <- event{Key: "DNSStart", Args: di} },
DNSDone: func(di httptrace.DNSDoneInfo) { eventQueue <- event{Key: "DNSDone", Args: di} },
ConnectStart: func(network, addr string) {
eventQueue <- event{Key: "ConnectStart", Args: map[string]string{"network": network, "addr": addr}}
},
ConnectDone: func(network, addr string, err error) {
eventQueue <- event{Key: "ConnectDone", Args: map[string]any{"network": network, "addr": addr, "err": err}}
},
TLSHandshakeStart: func() { eventQueue <- event{Key: "TLSHandshakeStart"} },
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
eventQueue <- event{Key: "TLSHandshakeDone", Args: map[string]any{"state": state, "err": err}}
},
WroteHeaderField: func(key string, value []string) {
if key != ":authority" {
return
}
eventQueue <- event{Key: "WroteHeaderField", Args: value[0]}
},
WroteHeaders: func() { eventQueue <- event{Key: "WroteHeaders"} },
Wait100Continue: func() { wait100Continue = true },
WroteRequest: func(i httptrace.WroteRequestInfo) { eventQueue <- event{Key: "WroteRequest", Args: i} },
}
ctx := httptrace.WithClientTrace(context.Background(), &trace)
tr := &http3.Transport{
TLSClientConfig: getTLSClientConfigWithoutServerName(),
QUICConfig: getQuicConfig(nil),
}
t.Cleanup(func() { tr.Close() })
cl := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/client-trace", port), nil)
require.NoError(t, err)
resp, err := cl.Do(req)
close(eventQueue)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
events := make([]string, 0, len(eventQueue))
for e := range eventQueue {
events = append(events, e.Key)
switch e.Key {
case "GetConn":
require.Equal(t, fmt.Sprintf("localhost:%d", port), e.Args.(string))
case "GotConn":
info := e.Args.(httptrace.GotConnInfo)
require.Equal(t, fmt.Sprintf("127.0.0.1:%d", port), info.Conn.RemoteAddr().String())
host, _, err := net.SplitHostPort(info.Conn.LocalAddr().String())
require.NoError(t, err)
require.Contains(t, []string{"::", "0.0.0.0"}, host)
require.Panics(t, func() { info.Conn.Close() })
require.Panics(t, func() { info.Conn.Read(buf) })
require.Panics(t, func() { info.Conn.Write(buf) })
require.Panics(t, func() { info.Conn.SetDeadline(time.Now()) })
require.Panics(t, func() { info.Conn.SetReadDeadline(time.Now()) })
require.Panics(t, func() { info.Conn.SetWriteDeadline(time.Now()) })
case "Got1xxResponse":
require.Equal(t, 100, e.Args.(int))
case "DNSStart":
require.Equal(t, "localhost", e.Args.(httptrace.DNSStartInfo).Host)
case "DNSDone":
require.Condition(t, func() bool {
localhost := net.IPv4(127, 0, 0, 1)
localhostTo16 := localhost.To16()
for _, addr := range e.Args.(httptrace.DNSDoneInfo).Addrs {
if addr.IP.Equal(localhost) || addr.IP.Equal(localhostTo16) {
return true
}
}
return false
})
case "ConnectStart":
require.Equal(t, "udp", e.Args.(map[string]string)["network"])
//require.Equal(t, fmt.Sprintf("127.0.0.1:%d", port), e.Args.(map[string]string)["addr"])
case "ConnectDone":
require.Equal(t, "udp", e.Args.(map[string]any)["network"])
//require.Equal(t, fmt.Sprintf("127.0.0.1:%d", port), e.Args.(map[string]any)["addr"])
require.Nil(t, e.Args.(map[string]any)["err"])
case "TLSHandshakeDone":
require.Nil(t, e.Args.(map[string]any)["err"])
state := e.Args.(map[string]any)["state"].(tls.ConnectionState)
require.Equal(t, 1, len(state.PeerCertificates))
require.Equal(t, "localhost", state.PeerCertificates[0].DNSNames[0])
case "WroteHeaderField":
require.Equal(t, fmt.Sprintf("localhost:%d", port), e.Args.(string))
case "WroteRequest":
require.NoError(t, e.Args.(httptrace.WroteRequestInfo).Err)
}
}
/*
require.Equal(t,
[]string{
"GetConn", "DNSStart", "DNSDone", "ConnectStart", "TLSHandshakeStart", "TLSHandshakeDone",
"ConnectDone", "GotConn", "WroteHeaderField", "WroteHeaders", "WroteRequest",
"GotFirstResponseByte", "Got1xxResponse", "Got100Continue",
}, events)
*/
require.Falsef(t, wait100Continue, "wait 100 continue") // Note: not supported Expect: 100-continue
}
|