Skip to content

Commit

Permalink
block Upgrade on HTTP SETTINGS, check for client datagram support (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Apr 26, 2024
1 parent 2a8e76e commit 0d0572a
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 22 deletions.
28 changes: 19 additions & 9 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,30 +201,40 @@ func TestClientReorderedUpgrade(t *testing.T) {

d := webtransport.Dialer{
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)
QUICConfig: &quic.Config{EnableDatagrams: true},
DialAddr: func(ctx context.Context, addr string, tlsConf *tls.Config, conf *quic.Config) (quic.EarlyConnection, error) {
conn, err := quic.DialAddrEarly(ctx, addr, tlsConf, conf)
if err != nil {
return nil, err
}
return &requestStreamDelayingConn{done: blockUpgrade, EarlyConnection: conn}, nil
},
}
connChan := make(chan *webtransport.Session)
sessChan := make(chan *webtransport.Session)
errChan := make(chan error)
go func() {
// This will block until blockUpgrade is closed.
rsp, conn, err := d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d/webtransport", port), nil)
require.NoError(t, err)
rsp, sess, err := d.Dial(context.Background(), fmt.Sprintf("https://localhost:%d/webtransport", port), nil)
if err != nil {
errChan <- err
return
}
require.Equal(t, 200, rsp.StatusCode)
connChan <- conn
sessChan <- sess
}()

time.Sleep(timeout)
close(blockUpgrade)
conn := <-connChan
defer conn.CloseWithError(0, "")
var sess *webtransport.Session
select {
case sess = <-sessChan:
case err := <-errChan:
require.NoError(t, err)
}
defer sess.CloseWithError(0, "")
ctx, cancel := context.WithTimeout(context.Background(), scaleDuration(100*time.Millisecond))
defer cancel()
str, err := conn.AcceptStream(ctx)
str, err := sess.AcceptStream(ctx)
require.NoError(t, err)
data, err := io.ReadAll(str)
require.NoError(t, err)
Expand Down
39 changes: 30 additions & 9 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ const (
type Server struct {
H3 http3.Server

// StreamReorderingTime is the time an incoming WebTransport stream that cannot be associated
// with a session is buffered.
// ReorderingTimeout is the maximum time an incoming WebTransport stream that cannot be associated
// with a session is buffered. It is also the maximum time a WebTransport connection request is
// blocked waiting for the client's SETTINGS are received.
// This can happen if the CONNECT request (that creates a new session) is reordered, and arrives
// after the first WebTransport stream(s) for that session.
// Defaults to 5 seconds.
StreamReorderingTimeout time.Duration
ReorderingTimeout time.Duration

// CheckOrigin is used to validate the request origin, thereby preventing cross-site request forgery.
// CheckOrigin returns true if the request Origin header is acceptable.
Expand All @@ -60,13 +61,18 @@ func (s *Server) initialize() error {
return s.initErr
}

func (s *Server) init() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
timeout := s.StreamReorderingTimeout
func (s *Server) timeout() time.Duration {
timeout := s.ReorderingTimeout
if timeout == 0 {
timeout = 5 * time.Second
return 5 * time.Second
}
s.conns = newSessionManager(timeout)
return timeout
}

func (s *Server) init() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background())

s.conns = newSessionManager(s.timeout())
if s.CheckOrigin == nil {
s.CheckOrigin = checkSameOrigin
}
Expand Down Expand Up @@ -168,6 +174,21 @@ func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*Session, erro
if !s.CheckOrigin(r) {
return nil, errors.New("webtransport: request origin not allowed")
}

// Wait for SETTINGS
conn := w.(http3.Hijacker).Connection()
timer := time.NewTimer(s.timeout())
defer timer.Stop()
select {
case <-conn.ReceivedSettings():
case <-timer.C:
return nil, errors.New("webtransport: didn't receive the client's SETTINGS on time")
}
settings := conn.Settings()
if !settings.EnableDatagram {
return nil, errors.New("webtransport: missing datagram support")
}

w.Header().Add(webTransportDraftHeaderKey, webTransportDraftHeaderValue)
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
Expand All @@ -177,7 +198,7 @@ func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*Session, erro
sID := sessionID(str.StreamID())

return s.conns.AddSession(
w.(http3.Hijacker).Connection(),
conn,
sID,
httpStreamer.HTTPStream(),
), nil
Expand Down
45 changes: 41 additions & 4 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ func TestServerReorderedUpgradeRequest(t *testing.T) {
func TestServerReorderedUpgradeRequestTimeout(t *testing.T) {
timeout := scaleDuration(100 * time.Millisecond)
s := webtransport.Server{
H3: http3.Server{TLSConfig: tlsConf, EnableDatagrams: true},
StreamReorderingTimeout: timeout,
H3: http3.Server{TLSConfig: tlsConf, EnableDatagrams: true},
ReorderingTimeout: timeout,
}
defer s.Close()
connChan := make(chan *webtransport.Session)
Expand Down Expand Up @@ -212,8 +212,8 @@ func TestServerReorderedUpgradeRequestTimeout(t *testing.T) {
func TestServerReorderedMultipleStreams(t *testing.T) {
timeout := scaleDuration(150 * time.Millisecond)
s := webtransport.Server{
H3: http3.Server{TLSConfig: tlsConf, EnableDatagrams: true},
StreamReorderingTimeout: timeout,
H3: http3.Server{TLSConfig: tlsConf, EnableDatagrams: true},
ReorderingTimeout: timeout,
}
defer s.Close()
connChan := make(chan *webtransport.Session)
Expand Down Expand Up @@ -273,6 +273,43 @@ func TestServerReorderedMultipleStreams(t *testing.T) {
require.Equal(t, []byte("raboof"), data)
}

func TestServerSettingsCheck(t *testing.T) {
timeout := scaleDuration(150 * time.Millisecond)
s := webtransport.Server{
H3: http3.Server{TLSConfig: tlsConf, EnableDatagrams: true},
ReorderingTimeout: timeout,
}
errChan := make(chan error, 1)
mux := http.NewServeMux()
mux.HandleFunc("/webtransport", func(w http.ResponseWriter, r *http.Request) {
_, err := s.Upgrade(w, r)
w.WriteHeader(http.StatusNotImplemented)
errChan <- err
})
s.H3.Handler = mux
udpConn, err := net.ListenUDP("udp", nil)
require.NoError(t, err)
port := udpConn.LocalAddr().(*net.UDPAddr).Port
go s.Serve(udpConn)

cconn, err := quic.DialAddr(
context.Background(),
fmt.Sprintf("localhost:%d", port),
&tls.Config{RootCAs: certPool, NextProtos: []string{http3.NextProtoH3}},
&quic.Config{EnableDatagrams: true},
)
require.NoError(t, err)
rt := http3.SingleDestinationRoundTripper{Connection: cconn} // datagrams disabled
requestStr, err := rt.OpenRequestStream(context.Background())
require.NoError(t, err)
require.NoError(t, requestStr.SendRequestHeader(newWebTransportRequest(t, fmt.Sprintf("https://localhost:%d/webtransport", port))))
rsp, err := requestStr.ReadResponse()
require.NoError(t, err)
require.Equal(t, http.StatusNotImplemented, rsp.StatusCode)

require.ErrorContains(t, <-errChan, "webtransport: missing datagram support")
}

func TestImmediateClose(t *testing.T) {
s := webtransport.Server{H3: http3.Server{}}
require.NoError(t, s.Close())
Expand Down

0 comments on commit 0d0572a

Please sign in to comment.