Skip to content

Commit

Permalink
use the new HTTP/3 API
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Apr 11, 2024
1 parent c5beefb commit e15953d
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 369 deletions.
150 changes: 83 additions & 67 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package webtransport

import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
Expand All @@ -17,12 +18,12 @@ import (
var errNoWebTransport = errors.New("server didn't enable WebTransport")

type Dialer struct {
// If not set, reasonable defaults will be used.
// In order for WebTransport to function, this implementation will:
// * overwrite the StreamHijacker and UniStreamHijacker
// * enable datagram support
// * set the MaxIncomingStreams to 100 on the quic.Config, if unset
*http3.RoundTripper
// TLSClientConfig is the TLS client config used when dialing the QUIC connection.
// It must set the h3 ALPN.
TLSClientConfig *tls.Config

// QUICConfig is the QUIC config used when dialing the QUIC connection.
QUICConfig *quic.Config

// StreamReorderingTime is the time an incoming WebTransport stream that cannot be associated
// with a session is buffered.
Expand All @@ -31,6 +32,10 @@ type Dialer struct {
// Defaults to 5 seconds.
StreamReorderingTimeout time.Duration

// DialAddr is the function used to dial the underlying QUIC connection.
// If unset, quic.DialAddrEarly will be used.
DialAddr func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error)

ctx context.Context
ctxCancel context.CancelFunc

Expand All @@ -46,53 +51,28 @@ func (d *Dialer) init() {
}
d.conns = *newSessionManager(timeout)
d.ctx, d.ctxCancel = context.WithCancel(context.Background())
if d.RoundTripper == nil {
d.RoundTripper = &http3.RoundTripper{}
}
d.RoundTripper.EnableDatagrams = true
if d.RoundTripper.AdditionalSettings == nil {
d.RoundTripper.AdditionalSettings = make(map[uint64]uint64)
}
d.RoundTripper.AdditionalSettings[settingsEnableWebtransport] = 1
d.RoundTripper.StreamHijacker = func(ft http3.FrameType, conn quic.Connection, str quic.Stream, e error) (hijacked bool, err error) {
if isWebTransportError(e) {
return true, nil
}
if ft != webTransportFrameType {
return false, nil
}
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if isWebTransportError(err) {
return true, nil
}
return false, err
}
d.conns.AddStream(conn, str, sessionID(id))
return true, nil
}
d.RoundTripper.UniStreamHijacker = func(st http3.StreamType, conn quic.Connection, str quic.ReceiveStream, err error) (hijacked bool) {
if st != webTransportUniStreamType && !isWebTransportError(err) {
return false
}
d.conns.AddUniStream(conn, str)
return true
}
if d.QuicConfig == nil {
d.QuicConfig = &quic.Config{EnableDatagrams: true}
}
if d.QuicConfig.MaxIncomingStreams == 0 {
d.QuicConfig.MaxIncomingStreams = 100
}
}

func (d *Dialer) Dial(ctx context.Context, urlStr string, reqHdr http.Header) (*http.Response, *Session, error) {
d.initOnce.Do(func() { d.init() })

// Technically, this is not true. DATAGRAMs could be sent using the Capsule protocol.
// However, quic-go currently enforces QUIC datagram support if HTTP/3 datagrams are enabled.
if !d.QuicConfig.EnableDatagrams {
return nil, nil, errors.New("WebTransport requires DATAGRAM support, enable it via QuicConfig.EnableDatagrams")
quicConf := d.QUICConfig
if quicConf == nil {
quicConf = &quic.Config{EnableDatagrams: true}
} else if !d.QUICConfig.EnableDatagrams {
return nil, nil, errors.New("WebTransport requires DATAGRAM support, enable it via QUICConfig.EnableDatagrams")
}

tlsConf := d.TLSClientConfig
if tlsConf == nil {
tlsConf = &tls.Config{}
} else {
tlsConf = tlsConf.Clone()
}
if len(tlsConf.NextProtos) == 0 {
tlsConf.NextProtos = []string{http3.NextProtoH3}
}

u, err := url.Parse(urlStr)
Expand All @@ -112,38 +92,74 @@ func (d *Dialer) Dial(ctx context.Context, urlStr string, reqHdr http.Header) (*
}
req = req.WithContext(ctx)

rsp, err := d.RoundTripper.RoundTripOpt(req, http3.RoundTripOpt{
DontCloseRequestStream: true,
CheckSettings: func(settings http3.Settings) error {
if !settings.EnableExtendedConnect {
return errors.New("server didn't enable Extended CONNECT")
dialAddr := d.DialAddr
if dialAddr == nil {
dialAddr = quic.DialAddrEarly
}
qconn, err := dialAddr(ctx, u.Host, tlsConf, quicConf)
if err != nil {
return nil, nil, err
}
rt := &http3.SingleDestinationRoundTripper{
Connection: qconn,
StreamHijacker: func(ft http3.FrameType, connTracingID quic.ConnectionTracingID, str quic.Stream, e error) (hijacked bool, err error) {
if isWebTransportError(e) {
return true, nil
}
if !settings.EnableDatagram {
return errors.New("server didn't enable HTTP/3 datagram support")
if ft != webTransportFrameType {
return false, nil
}
if settings.Other == nil {
return errNoWebTransport
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if isWebTransportError(err) {
return true, nil
}
return false, err
}
s, ok := settings.Other[settingsEnableWebtransport]
if !ok || s != 1 {
return errNoWebTransport
d.conns.AddStream(connTracingID, str, sessionID(id))
return true, nil
},
UniStreamHijacker: func(st http3.StreamType, connTracingID quic.ConnectionTracingID, str quic.ReceiveStream, err error) (hijacked bool) {
if st != webTransportUniStreamType && !isWebTransportError(err) {
return false
}
return nil
d.conns.AddUniStream(connTracingID, str)
return true
},
})
}

conn := rt.Start()
requestStr, err := rt.OpenRequestStream(ctx) // TODO: put this on the Connection (maybe introduce a ClientConnection?)
if err != nil {
return nil, nil, err
}
if err := requestStr.SendRequestHeader(req); err != nil {
return nil, nil, err
}
<-conn.ReceivedSettings() // TODO: select
settings := conn.Settings() // TODO: instead of putting the settings on the SingleDestinationRoundTripper, create a way to retrieve the Connection instead
if !settings.EnableExtendedConnect {
return nil, nil, errors.New("server didn't enable Extended CONNECT")
}
if !settings.EnableDatagram {
return nil, nil, errors.New("server didn't enable HTTP/3 datagram support")
}
if settings.Other == nil {
return nil, nil, errNoWebTransport
}
s, ok := settings.Other[settingsEnableWebtransport]
if !ok || s != 1 {
return nil, nil, errNoWebTransport
}

rsp, err := requestStr.ReadResponse()
if err != nil {
return nil, nil, err
}
if rsp.StatusCode < 200 || rsp.StatusCode >= 300 {
return rsp, nil, fmt.Errorf("received status %d", rsp.StatusCode)
}
str := rsp.Body.(http3.HTTPStreamer).HTTPStream()
conn := d.conns.AddSession(
rsp.Body.(http3.Hijacker).StreamCreator(),
sessionID(str.StreamID()),
str,
)
return rsp, conn, nil
return rsp, d.conns.AddSession(conn, sessionID(requestStr.StreamID()), requestStr), nil
}

func (d *Dialer) Close() error {
Expand Down
30 changes: 10 additions & 20 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,7 @@ func TestClientInvalidResponseHandling(t *testing.T) {
}
}()

d := webtransport.Dialer{
RoundTripper: &http3.RoundTripper{
TLSClientConfig: &tls.Config{RootCAs: certPool},
},
}
d := webtransport.Dialer{TLSClientConfig: &tls.Config{RootCAs: certPool}}
_, _, err = d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d", s.Addr().(*net.UDPAddr).Port), nil)
require.Error(t, err)
var sErr error
Expand Down Expand Up @@ -163,7 +159,7 @@ func TestClientInvalidSettingsHandling(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tlsConf := tlsConf.Clone()
tlsConf.NextProtos = []string{"h3"}
tlsConf.NextProtos = []string{http3.NextProtoH3}
s, err := quic.ListenAddr("localhost:0", tlsConf, &quic.Config{EnableDatagrams: true})
require.NoError(t, err)
go func() {
Expand All @@ -176,11 +172,7 @@ func TestClientInvalidSettingsHandling(t *testing.T) {
require.NoError(t, err)
}()

d := webtransport.Dialer{
RoundTripper: &http3.RoundTripper{
TLSClientConfig: &tls.Config{RootCAs: certPool},
},
}
d := webtransport.Dialer{TLSClientConfig: &tls.Config{RootCAs: certPool}}
_, _, err = d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d", s.Addr().(*net.UDPAddr).Port), nil)
require.Error(t, err)
require.ErrorContains(t, err, tc.errorStr)
Expand Down Expand Up @@ -208,15 +200,13 @@ func TestClientReorderedUpgrade(t *testing.T) {
go s.Serve(udpConn)

d := webtransport.Dialer{
RoundTripper: &http3.RoundTripper{
TLSClientConfig: &tls.Config{RootCAs: certPool},
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
conn, err := quic.DialAddrEarly(ctx, addr, tlsCfg, cfg)
if err != nil {
return nil, err
}
return &requestStreamDelayingConn{done: blockUpgrade, EarlyConnection: conn}, nil
},
TLSClientConfig: &tls.Config{RootCAs: certPool},
DialAddr: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
conn, err := quic.DialAddrEarly(ctx, addr, tlsCfg, cfg)
if err != nil {
return nil, err
}
return &requestStreamDelayingConn{done: blockUpgrade, EarlyConnection: conn}, nil
},
}
connChan := make(chan *webtransport.Session)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/quic-go/webtransport-go
go 1.21

require (
github.com/quic-go/quic-go v0.42.0
github.com/quic-go/quic-go v0.42.1-0.20240411165505-da410a7b5935
github.com/stretchr/testify v1.8.0
go.uber.org/mock v0.4.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/quic-go/quic-go v0.42.1-0.20240411165505-da410a7b5935 h1:gKMPe5jl70yeWH2AW2eHZ4Mva+rmxKIfOG2MKv6tmaU=
github.com/quic-go/quic-go v0.42.1-0.20240411165505-da410a7b5935/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
Expand Down
Loading

0 comments on commit e15953d

Please sign in to comment.