add vendor data
All checks were successful
Create and publish a Docker image 🚀 / build-and-push-image (push) Successful in 1m49s

This commit is contained in:
Smile Rex
2026-03-10 01:11:41 +03:00
parent d56b51065f
commit 6ace91a21a
962 changed files with 384706 additions and 2 deletions

2
vendor/github.com/quic-go/webtransport-go/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
*.qlog
*.sqlog

View File

@@ -0,0 +1,45 @@
version: "2"
linters:
default: none
enable:
- asciicheck
- copyloopvar
- depguard
- exhaustive
- govet
- ineffassign
- misspell
- nolintlint
- prealloc
- staticcheck
- unconvert
- unparam
- unused
- usetesting
settings:
depguard:
rules:
random:
deny:
- pkg: "math/rand$"
desc: use math/rand/v2
- pkg: "golang.org/x/exp/rand"
desc: use math/rand/v2
# see https://github.com/ldez/usetesting/issues/10
usetesting:
context-background: false
context-todo: false
exclusions:
rules:
- linters:
- staticcheck
text: 'SA1019:.+quic\.ConnectionTracing(ID|Key)'
- linters:
- staticcheck
path: _test\.go
text: 'SA1029:' # inappropriate key in call to context.WithValue
formatters:
enable:
- gofmt
- gofumpt
- goimports

7
vendor/github.com/quic-go/webtransport-go/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2022 Marten Seemann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

25
vendor/github.com/quic-go/webtransport-go/README.md generated vendored Normal file
View File

@@ -0,0 +1,25 @@
# webtransport-go
[![Documentation](https://img.shields.io/badge/docs-quic--go.net-red?style=flat)](https://quic-go.net/docs/)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/webtransport-go)](https://pkg.go.dev/github.com/quic-go/webtransport-go)
[![Code Coverage](https://img.shields.io/codecov/c/github/quic-go/webtransport-go/master.svg?style=flat-square)](https://codecov.io/gh/quic-go/webtransport-go/)
webtransport-go is an implementation of the WebTransport protocol, based on [quic-go](https://github.com/quic-go/quic-go). It currently implements [draft-02](https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-02.html) of the specification.
Detailed documentation can be found on [quic-go.net](https://quic-go.net/docs/).
## Projects using webtransport-go
| Project | Description | Stars |
| --- | --- | --- |
| [Centrifugo](https://github.com/centrifugal/centrifugo) | Scalable real-time messaging server in a language-agnostic way. Self-hosted alternative to Pubnub, Pusher, Ably, socket.io, Phoenix.PubSub, SignalR. | ![GitHub Repo stars](https://img.shields.io/github/stars/centrifugal/centrifugo?style=flat-square) |
| [go-libp2p](https://github.com/libp2p/go-libp2p) | libp2p implementation in Go, powering [Kubo](https://github.com/ipfs/kubo) (IPFS) and [Lotus](https://github.com/filecoin-project/lotus) (Filecoin), among others | ![GitHub Repo stars](https://img.shields.io/github/stars/libp2p/go-libp2p?style=flat-square) |
| [signalr](https://github.com/philippseith/signalr) | SignalR server and client in Go | ![GitHub Repo stars](https://img.shields.io/github/stars/philippseith/signalr?style=flat-square) |
If you'd like to see your project added to this list, please send us a PR.
## Release Policy
webtransport-go always aims to support the latest two Go releases.

14
vendor/github.com/quic-go/webtransport-go/SECURITY.md generated vendored Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
webtransport-go is an implementation of the WebTransport over HTTP/3 protocol. No software is perfect, and we take reports of potential security issues very seriously.
## Reporting a Vulnerability
If you discover a vulnerability that could affect production deployments (e.g., a remotely exploitable issue), please report it [**privately**](https://github.com/quic-go/webtransport-go/security/advisories/new).
Please **DO NOT file a public issue** for exploitable vulnerabilities.
If the issue is theoretical, non-exploitable, or related to an experimental feature, you may discuss it openly by filing a regular issue.
## Reporting a non-security bug
For bugs, feature requests, or other non-security concerns, please open a GitHub [issue](https://github.com/quic-go/webtransport-go/issues/new).

273
vendor/github.com/quic-go/webtransport-go/client.go generated vendored Normal file
View File

@@ -0,0 +1,273 @@
package webtransport
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"sync"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
"github.com/dunglas/httpsfv"
)
var errNoWebTransport = errors.New("server didn't enable WebTransport")
type Dialer struct {
// 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
// ApplicationProtocols is a list of application protocols that can be negotiated,
// see section 3.3 of https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-14 for details.
ApplicationProtocols []string
// StreamReorderingTime is the time an incoming WebTransport stream that cannot be associated
// with a session is buffered.
// This can happen if the response to a 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
// 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.Conn, error)
ctx context.Context
ctxCancel context.CancelFunc
initOnce sync.Once
}
func (d *Dialer) init() {
d.ctx, d.ctxCancel = context.WithCancel(context.Background())
}
func (d *Dialer) Dial(ctx context.Context, urlStr string, reqHdr http.Header) (*http.Response, *Session, error) {
d.initOnce.Do(func() { d.init() })
quicConf := d.QUICConfig
if quicConf == nil {
quicConf = &quic.Config{
EnableDatagrams: true,
EnableStreamResetPartialDelivery: true,
}
} else {
if !d.QUICConfig.EnableDatagrams {
return nil, nil, errors.New("webtransport: DATAGRAM support required, enable it via QUICConfig.EnableDatagrams")
}
if !d.QUICConfig.EnableStreamResetPartialDelivery {
return nil, nil, errors.New("webtransport: stream reset partial delivery required, enable it via QUICConfig.EnableStreamResetPartialDelivery")
}
}
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)
if err != nil {
return nil, nil, err
}
if reqHdr == nil {
reqHdr = http.Header{}
}
if len(d.ApplicationProtocols) > 0 && reqHdr.Get(wtAvailableProtocolsHeader) == "" {
list := httpsfv.List{}
for _, protocol := range d.ApplicationProtocols {
list = append(list, httpsfv.NewItem(protocol))
}
protocols, err := httpsfv.Marshal(list)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal application protocols: %w", err)
}
reqHdr.Set(wtAvailableProtocolsHeader, protocols)
}
req := &http.Request{
Method: http.MethodConnect,
Header: reqHdr,
Proto: "webtransport",
Host: u.Host,
URL: u,
}
req = req.WithContext(ctx)
dialAddr := d.DialAddr
if dialAddr == nil {
dialAddr = quic.DialAddrEarly
}
qconn, err := dialAddr(ctx, u.Host, tlsConf, quicConf)
if err != nil {
return nil, nil, err
}
tr := &http3.Transport{EnableDatagrams: true}
rsp, sess, err := d.handleConn(ctx, tr, qconn, req)
if err != nil {
// TODO: use a more specific error code
// see https://github.com/ietf-wg-webtrans/draft-ietf-webtrans-http3/issues/245
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
tr.Close()
return rsp, nil, err
}
context.AfterFunc(sess.Context(), func() {
qconn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
tr.Close()
})
return rsp, sess, nil
}
func (d *Dialer) handleConn(ctx context.Context, tr *http3.Transport, qconn *quic.Conn, req *http.Request) (*http.Response, *Session, error) {
timeout := d.StreamReorderingTimeout
if timeout == 0 {
timeout = 5 * time.Second
}
sessMgr := newSessionManager(timeout)
context.AfterFunc(qconn.Context(), sessMgr.Close)
conn := tr.NewRawClientConn(qconn)
go func() {
for {
str, err := qconn.AcceptStream(context.Background())
if err != nil {
return
}
go func() {
typ, err := quicvarint.Peek(str)
if err != nil {
return
}
if typ != webTransportFrameType {
conn.HandleBidirectionalStream(str)
return
}
// read the frame type (already peeked above)
if _, err := quicvarint.Read(quicvarint.NewReader(str)); err != nil {
return
}
// read the session ID
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
return
}
sessMgr.AddStream(str, sessionID(id))
}()
}
}()
go func() {
for {
str, err := qconn.AcceptUniStream(context.Background())
if err != nil {
return
}
go func() {
typ, err := quicvarint.Peek(str)
if err != nil {
return
}
if typ != webTransportUniStreamType {
conn.HandleUnidirectionalStream(str)
return
}
// read the stream type (already peeked above)
if _, err := quicvarint.Read(quicvarint.NewReader(str)); err != nil {
return
}
// read the session ID
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
str.CancelRead(quic.StreamErrorCode(http3.ErrCodeGeneralProtocolError))
return
}
sessMgr.AddUniStream(str, sessionID(id))
}()
}
}()
select {
case <-conn.ReceivedSettings():
case <-ctx.Done():
return nil, nil, fmt.Errorf("error waiting for HTTP/3 settings: %w", context.Cause(ctx))
case <-d.ctx.Done():
return nil, nil, context.Cause(d.ctx)
}
settings := conn.Settings()
if !settings.EnableExtendedConnect {
return nil, nil, errors.New("server didn't enable Extended CONNECT")
}
if !settings.EnableDatagrams {
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
}
requestStr, err := conn.OpenRequestStream(ctx)
if err != nil {
return nil, nil, err
}
if err := requestStr.SendRequestHeader(req); err != nil {
return nil, nil, err
}
// TODO(#136): create the session to allow optimistic opening of streams and sending of datagrams
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)
}
var protocol string
if protocolHeader, ok := rsp.Header[http.CanonicalHeaderKey(wtProtocolHeader)]; ok {
protocol = d.negotiateProtocol(protocolHeader)
}
sessID := sessionID(requestStr.StreamID())
sess := newSession(context.WithoutCancel(ctx), sessID, qconn, requestStr, protocol)
sessMgr.AddSession(sessID, sess)
return rsp, sess, nil
}
func (d *Dialer) negotiateProtocol(theirs []string) string {
negotiatedProtocolItem, err := httpsfv.UnmarshalItem(theirs)
if err != nil {
return ""
}
negotiatedProtocol, ok := negotiatedProtocolItem.Value.(string)
if !ok {
return ""
}
if !slices.Contains(d.ApplicationProtocols, negotiatedProtocol) {
return ""
}
return negotiatedProtocol
}
func (d *Dialer) Close() error {
d.ctxCancel()
return nil
}

10
vendor/github.com/quic-go/webtransport-go/codecov.yml generated vendored Normal file
View File

@@ -0,0 +1,10 @@
coverage:
ignore:
- interop/
status:
project:
default:
informational: true
patch:
default:
informational: true

76
vendor/github.com/quic-go/webtransport-go/errors.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
package webtransport
import (
"errors"
"fmt"
"github.com/quic-go/quic-go"
)
// StreamErrorCode is an error code used for stream termination.
type StreamErrorCode uint32
// SessionErrorCode is an error code for session termination.
type SessionErrorCode uint32
const (
firstErrorCode = 0x52e4a40fa8db
lastErrorCode = 0x52e5ac983162
)
func webtransportCodeToHTTPCode(n StreamErrorCode) quic.StreamErrorCode {
return quic.StreamErrorCode(firstErrorCode) + quic.StreamErrorCode(n) + quic.StreamErrorCode(n/0x1e)
}
func httpCodeToWebtransportCode(h quic.StreamErrorCode) (StreamErrorCode, error) {
if h < firstErrorCode || h > lastErrorCode {
return 0, errors.New("error code outside of expected range")
}
if (h-0x21)%0x1f == 0 {
return 0, errors.New("invalid error code")
}
shifted := h - firstErrorCode
return StreamErrorCode(shifted - shifted/0x1f), nil
}
const (
// WTBufferedStreamRejectedErrorCode is the error code of the
// WT_BUFFERED_STREAM_REJECTED error.
WTBufferedStreamRejectedErrorCode quic.StreamErrorCode = 0x3994bd84
// WTSessionGoneErrorCode is the error code of the WT_SESSION_GONE error.
WTSessionGoneErrorCode quic.StreamErrorCode = 0x170d7b68
)
// StreamError is the error that is returned from stream operations (Read, Write) when the stream is canceled.
type StreamError struct {
ErrorCode StreamErrorCode
Remote bool
}
var _ error = &StreamError{}
func (e *StreamError) Error() string {
return fmt.Sprintf("stream canceled with error code %d", e.ErrorCode)
}
func (e *StreamError) Is(target error) bool {
t, ok := target.(*StreamError)
return ok && t.Remote == e.Remote && t.ErrorCode == e.ErrorCode
}
// SessionError is a WebTransport connection error.
type SessionError struct {
Remote bool
ErrorCode SessionErrorCode
Message string
}
var _ error = &SessionError{}
func (e *SessionError) Error() string { return e.Message }
func (e *SessionError) Is(target error) bool {
t, ok := target.(*SessionError)
return ok && e.ErrorCode == t.ErrorCode && e.Remote == t.Remote
}

View File

@@ -0,0 +1,5 @@
package webtransport
const settingsEnableWebtransport = 0x2b603742
const protocolHeader = "webtransport"

445
vendor/github.com/quic-go/webtransport-go/server.go generated vendored Normal file
View File

@@ -0,0 +1,445 @@
package webtransport
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"slices"
"sync"
"time"
"unicode/utf8"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
"github.com/dunglas/httpsfv"
)
const (
wtAvailableProtocolsHeader = "WT-Available-Protocols"
wtProtocolHeader = "WT-Protocol"
)
const (
webTransportFrameType = 0x41
webTransportUniStreamType = 0x54
)
type quicConnKeyType struct{}
var quicConnKey = quicConnKeyType{}
func ConfigureHTTP3Server(s *http3.Server) {
if s.AdditionalSettings == nil {
s.AdditionalSettings = make(map[uint64]uint64, 1)
}
s.AdditionalSettings[settingsEnableWebtransport] = 1
s.EnableDatagrams = true
origConnContext := s.ConnContext
s.ConnContext = func(ctx context.Context, conn *quic.Conn) context.Context {
if origConnContext != nil {
ctx = origConnContext(ctx, conn)
}
ctx = context.WithValue(ctx, quicConnKey, conn)
return ctx
}
}
type Server struct {
H3 *http3.Server
// ApplicationProtocols is a list of application protocols that can be negotiated,
// see section 3.3 of https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-14 for details.
ApplicationProtocols []string
// 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.
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.
// If unset, a safe default is used: If the Origin header is set, it is checked that it
// matches the request's Host header.
CheckOrigin func(r *http.Request) bool
ctx context.Context // is closed when Close is called
ctxCancel context.CancelFunc
refCount sync.WaitGroup
initOnce sync.Once
initErr error
connsMx sync.Mutex
conns map[*quic.Conn]*sessionManager
}
func (s *Server) initialize() error {
s.initOnce.Do(func() {
s.initErr = s.init()
})
return s.initErr
}
func (s *Server) timeout() time.Duration {
timeout := s.ReorderingTimeout
if timeout == 0 {
return 5 * time.Second
}
return timeout
}
func (s *Server) init() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.conns = make(map[*quic.Conn]*sessionManager)
if s.CheckOrigin == nil {
s.CheckOrigin = checkSameOrigin
}
return nil
}
func (s *Server) Serve(conn net.PacketConn) error {
if err := s.initialize(); err != nil {
return err
}
var quicConf *quic.Config
if s.H3.QUICConfig != nil {
quicConf = s.H3.QUICConfig.Clone()
} else {
quicConf = &quic.Config{}
}
quicConf.EnableDatagrams = true
quicConf.EnableStreamResetPartialDelivery = true
ln, err := quic.ListenEarly(conn, s.H3.TLSConfig, quicConf)
if err != nil {
return err
}
defer ln.Close()
for {
qconn, err := ln.Accept(s.ctx)
if err != nil {
return err
}
s.refCount.Add(1)
go func() {
defer s.refCount.Done()
if err := s.ServeQUICConn(qconn); err != nil {
log.Printf("http3: error serving QUIC connection: %v", err)
}
}()
}
}
// ServeQUICConn serves a single QUIC connection.
func (s *Server) ServeQUICConn(conn *quic.Conn) error {
connState := conn.ConnectionState()
if !connState.SupportsDatagrams.Local {
return errors.New("webtransport: QUIC DATAGRAM support required, enable it via QUICConfig.EnableDatagrams")
}
if !connState.SupportsStreamResetPartialDelivery.Local {
return errors.New("webtransport: QUIC Stream Resets with Partial Delivery required, enable it via QUICConfig.EnableStreamResetPartialDelivery")
}
if err := s.initialize(); err != nil {
return err
}
s.connsMx.Lock()
sessMgr, ok := s.conns[conn]
if !ok {
sessMgr = newSessionManager(s.timeout())
s.conns[conn] = sessMgr
}
s.connsMx.Unlock()
// Clean up when connection closes
context.AfterFunc(conn.Context(), func() {
s.connsMx.Lock()
delete(s.conns, conn)
s.connsMx.Unlock()
sessMgr.Close()
})
http3Conn, err := s.H3.NewRawServerConn(conn)
if err != nil {
return err
}
// slose the connection when the server context is cancelled.
go func() {
select {
case <-s.ctx.Done():
conn.CloseWithError(0, "")
case <-conn.Context().Done():
// connection already closed
}
}()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for {
str, err := conn.AcceptStream(s.ctx)
if err != nil {
return
}
wg.Add(1)
go func() {
defer wg.Done()
typ, err := quicvarint.Peek(str)
if err != nil {
return
}
if typ != webTransportFrameType {
http3Conn.HandleRequestStream(str)
return
}
// read the frame type (already peeked)
if _, err := quicvarint.Read(quicvarint.NewReader(str)); err != nil {
return
}
// read the session ID
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
str.CancelRead(quic.StreamErrorCode(http3.ErrCodeGeneralProtocolError))
str.CancelWrite(quic.StreamErrorCode(http3.ErrCodeGeneralProtocolError))
return
}
sessMgr.AddStream(str, sessionID(id))
}()
}
}()
go func() {
defer wg.Done()
for {
str, err := conn.AcceptUniStream(s.ctx)
if err != nil {
return
}
wg.Add(1)
go func() {
defer wg.Done()
typ, err := quicvarint.Peek(str)
if err != nil {
return
}
if typ != webTransportUniStreamType {
http3Conn.HandleUnidirectionalStream(str)
return
}
// read the stream type (already peeked) before passing to AddUniStream
r := quicvarint.NewReader(str)
if _, err := quicvarint.Read(r); err != nil {
return
}
// read the session ID
id, err := quicvarint.Read(r)
if err != nil {
str.CancelRead(quic.StreamErrorCode(http3.ErrCodeGeneralProtocolError))
return
}
sessMgr.AddUniStream(str, sessionID(id))
}()
}
}()
wg.Wait()
return nil
}
func (s *Server) ListenAndServe() error {
addr := s.H3.Addr
if addr == "" {
addr = ":https"
}
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return err
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return err
}
return s.Serve(conn)
}
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
if s.H3.TLSConfig == nil {
s.H3.TLSConfig = &tls.Config{}
}
s.H3.TLSConfig.Certificates = []tls.Certificate{cert}
return s.ListenAndServe()
}
func (s *Server) Close() error {
// Make sure that ctxCancel is defined.
// This is expected to be uncommon.
// It only happens if the server is closed without Serve / ListenAndServe having been called.
s.initOnce.Do(func() {})
if s.ctxCancel != nil {
s.ctxCancel()
}
s.connsMx.Lock()
if s.conns != nil {
for _, mgr := range s.conns {
mgr.Close()
}
s.conns = nil
}
s.connsMx.Unlock()
err := s.H3.Close()
s.refCount.Wait()
return err
}
func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*Session, error) {
if err := s.initialize(); err != nil {
return nil, err
}
if r.Method != http.MethodConnect {
return nil, fmt.Errorf("expected CONNECT request, got %s", r.Method)
}
if r.Proto != protocolHeader {
return nil, fmt.Errorf("unexpected protocol: %s", r.Proto)
}
if !s.CheckOrigin(r) {
return nil, errors.New("webtransport: request origin not allowed")
}
id := r.Context().Value(quicConnKey)
if id == nil {
return nil, errors.New("webtransport: missing QUIC connection")
}
conn := id.(*quic.Conn)
selectedProtocol := s.selectProtocol(r.Header[http.CanonicalHeaderKey(wtAvailableProtocolsHeader)])
// Wait for SETTINGS
settingser := w.(http3.Settingser)
timer := time.NewTimer(s.timeout())
defer timer.Stop()
select {
case <-settingser.ReceivedSettings():
case <-timer.C:
return nil, errors.New("webtransport: didn't receive the client's SETTINGS on time")
}
settings := settingser.Settings()
if !settings.EnableDatagrams {
return nil, errors.New("webtransport: missing datagram support")
}
if selectedProtocol != "" {
v, err := httpsfv.Marshal(httpsfv.NewItem(selectedProtocol))
if err != nil {
return nil, fmt.Errorf("failed to marshal selected protocol: %w", err)
}
w.Header().Add(wtProtocolHeader, v)
}
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
str := w.(http3.HTTPStreamer).HTTPStream()
sessID := sessionID(str.StreamID())
// The session manager should already exist because ServeQUICConn creates it
// before any HTTP requests can be processed on this connection.
s.connsMx.Lock()
defer s.connsMx.Unlock()
sessMgr, ok := s.conns[conn]
if !ok {
return nil, errors.New("webtransport: connection session manager not found")
}
sess := newSession(context.WithoutCancel(r.Context()), sessID, conn, str, selectedProtocol)
sessMgr.AddSession(sessID, sess)
return sess, nil
}
func (s *Server) selectProtocol(theirs []string) string {
list, err := httpsfv.UnmarshalList(theirs)
if err != nil {
return ""
}
offered := make([]string, 0, len(list))
for _, item := range list {
i, ok := item.(httpsfv.Item)
if !ok {
return ""
}
protocol, ok := i.Value.(string)
if !ok {
return ""
}
offered = append(offered, protocol)
}
var selectedProtocol string
for _, p := range offered {
if slices.Contains(s.ApplicationProtocols, p) {
selectedProtocol = p
break
}
}
return selectedProtocol
}
// copied from https://github.com/gorilla/websocket
func checkSameOrigin(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
u, err := url.Parse(origin)
if err != nil {
return false
}
return equalASCIIFold(u.Host, r.Host)
}
// copied from https://github.com/gorilla/websocket
func equalASCIIFold(s, t string) bool {
for s != "" && t != "" {
sr, size := utf8.DecodeRuneInString(s)
s = s[size:]
tr, size := utf8.DecodeRuneInString(t)
t = t[size:]
if sr == tr {
continue
}
if 'A' <= sr && sr <= 'Z' {
sr = sr + 'a' - 'A'
}
if 'A' <= tr && tr <= 'Z' {
tr = tr + 'a' - 'A'
}
if sr != tr {
return false
}
}
return s == t
}

467
vendor/github.com/quic-go/webtransport-go/session.go generated vendored Normal file
View File

@@ -0,0 +1,467 @@
package webtransport
import (
"context"
"encoding/binary"
"io"
"math/rand/v2"
"net"
"sync"
"time"
"unicode/utf8"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
)
// sessionID is the WebTransport Session ID
type sessionID uint64
const closeSessionCapsuleType http3.CapsuleType = 0x2843
const maxCloseCapsuleErrorMsgLen = 1024
type acceptQueue[T any] struct {
mx sync.Mutex
// The channel is used to notify consumers (via Chan) about new incoming items.
// Needs to be buffered to preserve the notification if an item is enqueued
// between a call to Next and to Chan.
c chan struct{}
// Contains all the streams waiting to be accepted.
// There's no explicit limit to the length of the queue, but it is implicitly
// limited by the stream flow control provided by QUIC.
queue []T
}
func newAcceptQueue[T any]() *acceptQueue[T] {
return &acceptQueue[T]{c: make(chan struct{}, 1)}
}
func (q *acceptQueue[T]) Add(str T) {
q.mx.Lock()
q.queue = append(q.queue, str)
q.mx.Unlock()
select {
case q.c <- struct{}{}:
default:
}
}
func (q *acceptQueue[T]) Next() T {
q.mx.Lock()
defer q.mx.Unlock()
if len(q.queue) == 0 {
return *new(T)
}
str := q.queue[0]
q.queue = q.queue[1:]
return str
}
func (q *acceptQueue[T]) Chan() <-chan struct{} { return q.c }
type http3Stream interface {
io.ReadWriteCloser
ReceiveDatagram(context.Context) ([]byte, error)
SendDatagram([]byte) error
CancelRead(quic.StreamErrorCode)
CancelWrite(quic.StreamErrorCode)
SetWriteDeadline(time.Time) error
}
var (
_ http3Stream = &http3.Stream{}
_ http3Stream = &http3.RequestStream{}
)
// SessionState contains the state of a WebTransport session
type SessionState struct {
// ConnectionState contains the QUIC connection state, including TLS handshake information
ConnectionState quic.ConnectionState
// ApplicationProtocol contains the application protocol negotiated for the session
ApplicationProtocol string
}
type Session struct {
sessionID sessionID
conn *quic.Conn
str http3Stream
applicationProtocol string
streamHdr []byte
uniStreamHdr []byte
ctx context.Context
closeMx sync.Mutex
closeErr error // not nil once the session is closed
// streamCtxs holds all the context.CancelFuncs of calls to Open{Uni}StreamSync calls currently active.
// When the session is closed, this allows us to cancel all these contexts and make those calls return.
streamCtxs map[int]context.CancelFunc
bidiAcceptQueue acceptQueue[*Stream]
uniAcceptQueue acceptQueue[*ReceiveStream]
streams streamsMap
}
func newSession(ctx context.Context, sessionID sessionID, conn *quic.Conn, str http3Stream, applicationProtocol string) *Session {
ctx, ctxCancel := context.WithCancel(ctx)
c := &Session{
sessionID: sessionID,
conn: conn,
str: str,
applicationProtocol: applicationProtocol,
ctx: ctx,
streamCtxs: make(map[int]context.CancelFunc),
bidiAcceptQueue: *newAcceptQueue[*Stream](),
uniAcceptQueue: *newAcceptQueue[*ReceiveStream](),
streams: *newStreamsMap(),
}
// precompute the headers for unidirectional streams
c.uniStreamHdr = make([]byte, 0, 2+quicvarint.Len(uint64(c.sessionID)))
c.uniStreamHdr = quicvarint.Append(c.uniStreamHdr, webTransportUniStreamType)
c.uniStreamHdr = quicvarint.Append(c.uniStreamHdr, uint64(c.sessionID))
// precompute the headers for bidirectional streams
c.streamHdr = make([]byte, 0, 2+quicvarint.Len(uint64(c.sessionID)))
c.streamHdr = quicvarint.Append(c.streamHdr, webTransportFrameType)
c.streamHdr = quicvarint.Append(c.streamHdr, uint64(c.sessionID))
go func() {
defer ctxCancel()
c.handleConn()
}()
return c
}
func (s *Session) handleConn() {
err := s.parseNextCapsule()
s.closeWithError(err)
}
// parseNextCapsule parses the next Capsule sent on the request stream.
// It returns a SessionError, if the capsule received is a WT_CLOSE_SESSION Capsule.
func (s *Session) parseNextCapsule() error {
for {
typ, r, err := http3.ParseCapsule(quicvarint.NewReader(s.str))
if err != nil {
return err
}
switch typ {
case closeSessionCapsuleType:
var b [4]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
appErrCode := binary.BigEndian.Uint32(b[:])
// the length of the error message is limited to 1024 bytes
appErrMsg, err := io.ReadAll(io.LimitReader(r, maxCloseCapsuleErrorMsgLen))
if err != nil {
return err
}
return &SessionError{
Remote: true,
ErrorCode: SessionErrorCode(appErrCode),
Message: string(appErrMsg),
}
default:
// unknown capsule, skip it
if _, err := io.ReadAll(r); err != nil {
return err
}
}
}
}
func (s *Session) addStream(qstr *quic.Stream, addStreamHeader bool) *Stream {
var hdr []byte
if addStreamHeader {
hdr = s.streamHdr
}
str := newStream(qstr, hdr, func() { s.streams.RemoveStream(qstr.StreamID()) })
s.streams.AddStream(qstr.StreamID(), str.closeWithSession)
return str
}
func (s *Session) addReceiveStream(qstr *quic.ReceiveStream) *ReceiveStream {
str := newReceiveStream(qstr, func() { s.streams.RemoveStream(qstr.StreamID()) })
s.streams.AddStream(qstr.StreamID(), str.closeWithSession)
return str
}
func (s *Session) addSendStream(qstr *quic.SendStream) *SendStream {
str := newSendStream(qstr, s.uniStreamHdr, func() { s.streams.RemoveStream(qstr.StreamID()) })
s.streams.AddStream(qstr.StreamID(), str.closeWithSession)
return str
}
// addIncomingStream adds a bidirectional stream that the remote peer opened
func (s *Session) addIncomingStream(qstr *quic.Stream) {
s.closeMx.Lock()
closeErr := s.closeErr
if closeErr != nil {
s.closeMx.Unlock()
qstr.CancelRead(WTSessionGoneErrorCode)
qstr.CancelWrite(WTSessionGoneErrorCode)
return
}
str := s.addStream(qstr, false)
s.closeMx.Unlock()
s.bidiAcceptQueue.Add(str)
}
// addIncomingUniStream adds a unidirectional stream that the remote peer opened
func (s *Session) addIncomingUniStream(qstr *quic.ReceiveStream) {
s.closeMx.Lock()
closeErr := s.closeErr
if closeErr != nil {
s.closeMx.Unlock()
qstr.CancelRead(WTSessionGoneErrorCode)
return
}
str := s.addReceiveStream(qstr)
s.closeMx.Unlock()
s.uniAcceptQueue.Add(str)
}
// Context returns a context that is closed when the session is closed.
func (s *Session) Context() context.Context {
return s.ctx
}
func (s *Session) AcceptStream(ctx context.Context) (*Stream, error) {
s.closeMx.Lock()
closeErr := s.closeErr
s.closeMx.Unlock()
if closeErr != nil {
return nil, closeErr
}
for {
// If there's a stream in the accept queue, return it immediately.
if str := s.bidiAcceptQueue.Next(); str != nil {
return str, nil
}
// No stream in the accept queue. Wait until we accept one.
select {
case <-s.ctx.Done():
return nil, s.closeErr
case <-ctx.Done():
return nil, ctx.Err()
case <-s.bidiAcceptQueue.Chan():
}
}
}
func (s *Session) AcceptUniStream(ctx context.Context) (*ReceiveStream, error) {
s.closeMx.Lock()
closeErr := s.closeErr
s.closeMx.Unlock()
if closeErr != nil {
return nil, s.closeErr
}
for {
// If there's a stream in the accept queue, return it immediately.
if str := s.uniAcceptQueue.Next(); str != nil {
return str, nil
}
// No stream in the accept queue. Wait until we accept one.
select {
case <-s.ctx.Done():
return nil, s.closeErr
case <-ctx.Done():
return nil, ctx.Err()
case <-s.uniAcceptQueue.Chan():
}
}
}
func (s *Session) OpenStream() (*Stream, error) {
s.closeMx.Lock()
defer s.closeMx.Unlock()
if s.closeErr != nil {
return nil, s.closeErr
}
qstr, err := s.conn.OpenStream()
if err != nil {
return nil, err
}
return s.addStream(qstr, true), nil
}
func (s *Session) addStreamCtxCancel(cancel context.CancelFunc) (id int) {
rand:
id = rand.Int()
if _, ok := s.streamCtxs[id]; ok {
goto rand
}
s.streamCtxs[id] = cancel
return id
}
func (s *Session) OpenStreamSync(ctx context.Context) (*Stream, error) {
s.closeMx.Lock()
if s.closeErr != nil {
s.closeMx.Unlock()
return nil, s.closeErr
}
ctx, cancel := context.WithCancel(ctx)
id := s.addStreamCtxCancel(cancel)
s.closeMx.Unlock()
// open a new bidirectional stream without holding the mutex: this call might block
qstr, err := s.conn.OpenStreamSync(ctx)
s.closeMx.Lock()
defer s.closeMx.Unlock()
delete(s.streamCtxs, id)
// the session might have been closed concurrently with OpenStreamSync returning
if qstr != nil && s.closeErr != nil {
qstr.CancelRead(WTSessionGoneErrorCode)
qstr.CancelWrite(WTSessionGoneErrorCode)
return nil, s.closeErr
}
if err != nil {
if s.closeErr != nil {
return nil, s.closeErr
}
return nil, err
}
return s.addStream(qstr, true), nil
}
func (s *Session) OpenUniStream() (*SendStream, error) {
s.closeMx.Lock()
defer s.closeMx.Unlock()
if s.closeErr != nil {
return nil, s.closeErr
}
qstr, err := s.conn.OpenUniStream()
if err != nil {
return nil, err
}
return s.addSendStream(qstr), nil
}
func (s *Session) OpenUniStreamSync(ctx context.Context) (str *SendStream, err error) {
s.closeMx.Lock()
if s.closeErr != nil {
s.closeMx.Unlock()
return nil, s.closeErr
}
ctx, cancel := context.WithCancel(ctx)
id := s.addStreamCtxCancel(cancel)
s.closeMx.Unlock()
// open a new unidirectional stream without holding the mutex: this call might block
qstr, err := s.conn.OpenUniStreamSync(ctx)
s.closeMx.Lock()
defer s.closeMx.Unlock()
delete(s.streamCtxs, id)
// the session might have been closed concurrently with OpenStreamSync returning
if qstr != nil && s.closeErr != nil {
qstr.CancelWrite(WTSessionGoneErrorCode)
return nil, s.closeErr
}
if err != nil {
if s.closeErr != nil {
return nil, s.closeErr
}
return nil, err
}
return s.addSendStream(qstr), nil
}
func (s *Session) LocalAddr() net.Addr {
return s.conn.LocalAddr()
}
func (s *Session) RemoteAddr() net.Addr {
return s.conn.RemoteAddr()
}
func (s *Session) CloseWithError(code SessionErrorCode, msg string) error {
first, err := s.closeWithError(&SessionError{ErrorCode: code, Message: msg})
if err != nil || !first {
return err
}
// truncate the message if it's too long
if len(msg) > maxCloseCapsuleErrorMsgLen {
msg = truncateUTF8(msg, maxCloseCapsuleErrorMsgLen)
}
b := make([]byte, 4, 4+len(msg))
binary.BigEndian.PutUint32(b, uint32(code))
b = append(b, []byte(msg)...)
// Optimistically send the WT_CLOSE_SESSION Capsule:
// If we're flow-control limited, we don't want to wait for the receiver to issue new flow control credits.
// There's no idiomatic way to do a non-blocking write in Go, so we set a short deadline.
s.str.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
if err := http3.WriteCapsule(quicvarint.NewWriter(s.str), closeSessionCapsuleType, b); err != nil {
s.str.CancelWrite(WTSessionGoneErrorCode)
}
s.str.CancelRead(WTSessionGoneErrorCode)
err = s.str.Close()
<-s.ctx.Done()
return err
}
func (s *Session) SendDatagram(b []byte) error {
return s.str.SendDatagram(b)
}
func (s *Session) ReceiveDatagram(ctx context.Context) ([]byte, error) {
return s.str.ReceiveDatagram(ctx)
}
func (s *Session) closeWithError(closeErr error) (bool /* first call to close session */, error) {
s.closeMx.Lock()
defer s.closeMx.Unlock()
// Duplicate call, or the remote already closed this session.
if s.closeErr != nil {
return false, nil
}
s.closeErr = closeErr
for _, cancel := range s.streamCtxs {
cancel()
}
s.streams.CloseSession(closeErr)
return true, nil
}
// SessionState returns the current state of the session
func (s *Session) SessionState() SessionState {
return SessionState{
ConnectionState: s.conn.ConnectionState(),
ApplicationProtocol: s.applicationProtocol,
}
}
// truncateUTF8 cuts a string to max n bytes without breaking UTF-8 characters.
func truncateUTF8(s string, n int) string {
if len(s) <= n {
return s
}
for n > 0 && !utf8.RuneStart(s[n]) {
n--
}
return s[:n]
}

View File

@@ -0,0 +1,179 @@
package webtransport
import (
"context"
"slices"
"sync"
"time"
"github.com/quic-go/quic-go"
)
type unestablishedSession struct {
Streams []*quic.Stream
UniStreams []*quic.ReceiveStream
Timer *time.Timer
}
type sessionEntry struct {
// at any point in time, only one of these will be non-nil
Unestablished *unestablishedSession
Session *Session
}
const maxRecentlyClosedSessions = 16
type sessionManager struct {
timeout time.Duration
mx sync.Mutex
sessions map[sessionID]sessionEntry
recentlyClosedSessions []sessionID
}
func newSessionManager(timeout time.Duration) *sessionManager {
return &sessionManager{
timeout: timeout,
sessions: make(map[sessionID]sessionEntry),
}
}
// AddStream adds a new bidirectional stream to a WebTransport session.
// If the WebTransport session has not yet been established,
// the stream is buffered until the session is established.
// If that takes longer than timeout, the stream is reset.
func (m *sessionManager) AddStream(str *quic.Stream, id sessionID) {
m.mx.Lock()
defer m.mx.Unlock()
entry, ok := m.sessions[id]
if !ok {
// Receiving a stream for an unknown session is expected to be rare,
// so the performance impact of searching through the slice is negligible.
if slices.Contains(m.recentlyClosedSessions, id) {
str.CancelRead(WTBufferedStreamRejectedErrorCode)
str.CancelWrite(WTBufferedStreamRejectedErrorCode)
return
}
entry = sessionEntry{Unestablished: &unestablishedSession{}}
m.sessions[id] = entry
}
if entry.Session != nil {
entry.Session.addIncomingStream(str)
return
}
entry.Unestablished.Streams = append(entry.Unestablished.Streams, str)
m.resetTimer(id)
}
// AddUniStream adds a new unidirectional stream to a WebTransport session.
// If the WebTransport session has not yet been established,
// the stream is buffered until the session is established.
// If that takes longer than timeout, the stream is reset.
func (m *sessionManager) AddUniStream(str *quic.ReceiveStream, id sessionID) {
m.mx.Lock()
defer m.mx.Unlock()
entry, ok := m.sessions[id]
if !ok {
// Receiving a stream for an unknown session is expected to be rare,
// so the performance impact of searching through the slice is negligible.
if slices.Contains(m.recentlyClosedSessions, id) {
str.CancelRead(WTBufferedStreamRejectedErrorCode)
return
}
entry = sessionEntry{Unestablished: &unestablishedSession{}}
m.sessions[id] = entry
}
if entry.Session != nil {
entry.Session.addIncomingUniStream(str)
return
}
entry.Unestablished.UniStreams = append(entry.Unestablished.UniStreams, str)
m.resetTimer(id)
}
func (m *sessionManager) resetTimer(id sessionID) {
entry := m.sessions[id]
if entry.Unestablished.Timer != nil {
entry.Unestablished.Timer.Reset(m.timeout)
return
}
entry.Unestablished.Timer = time.AfterFunc(m.timeout, func() { m.onTimer(id) })
}
func (m *sessionManager) onTimer(id sessionID) {
m.mx.Lock()
defer m.mx.Unlock()
sessionEntry, ok := m.sessions[id]
if !ok { // session already closed
return
}
if sessionEntry.Session != nil { // session already established
return
}
for _, str := range sessionEntry.Unestablished.Streams {
str.CancelRead(WTBufferedStreamRejectedErrorCode)
str.CancelWrite(WTBufferedStreamRejectedErrorCode)
}
for _, uniStr := range sessionEntry.Unestablished.UniStreams {
uniStr.CancelRead(WTBufferedStreamRejectedErrorCode)
}
delete(m.sessions, id)
}
// AddSession adds a new WebTransport session.
func (m *sessionManager) AddSession(id sessionID, s *Session) {
m.mx.Lock()
defer m.mx.Unlock()
entry, ok := m.sessions[id]
if ok && entry.Unestablished != nil {
// We might already have an entry of this session.
// This can happen when we receive streams for this WebTransport session before we complete
// the Extended CONNECT request.
for _, str := range entry.Unestablished.Streams {
s.addIncomingStream(str)
}
for _, uniStr := range entry.Unestablished.UniStreams {
s.addIncomingUniStream(uniStr)
}
if entry.Unestablished.Timer != nil {
entry.Unestablished.Timer.Stop()
}
entry.Unestablished = nil
}
m.sessions[id] = sessionEntry{Session: s}
context.AfterFunc(s.Context(), func() {
m.deleteSession(id)
})
}
func (m *sessionManager) deleteSession(id sessionID) {
m.mx.Lock()
defer m.mx.Unlock()
delete(m.sessions, id)
m.recentlyClosedSessions = append(m.recentlyClosedSessions, id)
if len(m.recentlyClosedSessions) > maxRecentlyClosedSessions {
m.recentlyClosedSessions = m.recentlyClosedSessions[1:]
}
}
func (m *sessionManager) Close() {
m.mx.Lock()
defer m.mx.Unlock()
for _, entry := range m.sessions {
if entry.Unestablished != nil && entry.Unestablished.Timer != nil {
entry.Unestablished.Timer.Stop()
}
}
clear(m.sessions)
}

478
vendor/github.com/quic-go/webtransport-go/stream.go generated vendored Normal file
View File

@@ -0,0 +1,478 @@
package webtransport
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"sync"
"time"
"github.com/quic-go/quic-go"
)
type quicSendStream interface {
io.WriteCloser
CancelWrite(quic.StreamErrorCode)
Context() context.Context
SetWriteDeadline(time.Time) error
SetReliableBoundary()
}
var (
_ quicSendStream = &quic.SendStream{}
_ quicSendStream = &quic.Stream{}
)
type quicReceiveStream interface {
io.Reader
CancelRead(quic.StreamErrorCode)
SetReadDeadline(time.Time) error
}
var (
_ quicReceiveStream = &quic.ReceiveStream{}
_ quicReceiveStream = &quic.Stream{}
)
// A SendStream is a unidirectional WebTransport send stream.
type SendStream struct {
str quicSendStream
// WebTransport stream header.
// Set by the constructor, set to nil once sent out.
// Might be initialized to nil if this sendStream is part of an incoming bidirectional stream.
streamHdr []byte
streamHdrMu sync.Mutex
// Set to true when a goroutine is spawned to send the header asynchronously.
// This only happens if the stream is closed / reset immediately after creation.
sendingHdrAsync bool
onClose func() // to remove the stream from the streamsMap
closeOnce sync.Once
closed chan struct{}
closeErr error
deadlineMu sync.Mutex
writeDeadline time.Time
deadlineNotifyCh chan struct{} // receives a value when deadline changes
}
func newSendStream(str quicSendStream, hdr []byte, onClose func()) *SendStream {
return &SendStream{
str: str,
closed: make(chan struct{}),
streamHdr: hdr,
onClose: onClose,
}
}
// Write writes data to the stream.
// Write can be made to time out using [SendStream.SetWriteDeadline].
// If the stream was canceled, the error is a [StreamError].
func (s *SendStream) Write(b []byte) (int, error) {
n, err := s.write(b)
if err != nil && !isTimeoutError(err) {
s.onClose()
}
var strErr *quic.StreamError
if errors.As(err, &strErr) && strErr.ErrorCode == WTSessionGoneErrorCode {
return n, s.handleSessionGoneError()
}
return n, maybeConvertStreamError(err)
}
// handleSessionGoneError waits for the session to be closed after receiving a WTSessionGoneErrorCode.
// If the peer is initiating the session close, we might need to wait for the CONNECT stream to be closed.
// While a malicious peer might withhold the session close, this is not an interesting attack vector:
// 1. a WebTransport stream consumes very little memory, and
// 2. the number of concurrent WebTransport sessions is limited.
func (s *SendStream) handleSessionGoneError() error {
s.deadlineMu.Lock()
if s.deadlineNotifyCh == nil {
s.deadlineNotifyCh = make(chan struct{}, 1)
}
s.deadlineMu.Unlock()
for {
s.deadlineMu.Lock()
deadline := s.writeDeadline
s.deadlineMu.Unlock()
var timerCh <-chan time.Time
if !deadline.IsZero() {
if d := time.Until(deadline); d > 0 {
timerCh = time.After(d)
} else {
return os.ErrDeadlineExceeded
}
}
select {
case <-s.closed:
return s.closeErr
case <-timerCh:
return os.ErrDeadlineExceeded
case <-s.deadlineNotifyCh:
}
}
}
func (s *SendStream) write(b []byte) (int, error) {
s.streamHdrMu.Lock()
err := s.maybeSendStreamHeader()
s.streamHdrMu.Unlock()
if err != nil {
return 0, err
}
return s.str.Write(b)
}
func (s *SendStream) maybeSendStreamHeader() error {
if len(s.streamHdr) == 0 {
return nil
}
n, err := s.str.Write(s.streamHdr)
if n > 0 {
s.streamHdr = s.streamHdr[n:]
}
s.str.SetReliableBoundary()
if err != nil {
return err
}
s.streamHdr = nil
return nil
}
// CancelWrite aborts sending on this stream.
// Data already written, but not yet delivered to the peer is not guaranteed to be delivered reliably.
// Write will unblock immediately, and future calls to Write will fail.
// When called multiple times it is a no-op.
func (s *SendStream) CancelWrite(e StreamErrorCode) {
// if a Goroutine is already sending the header, return immediately
s.streamHdrMu.Lock()
if s.sendingHdrAsync {
s.streamHdrMu.Unlock()
return
}
if len(s.streamHdr) > 0 {
// Sending the stream header might block if we are blocked by flow control.
// Send a stream header async so that CancelWrite can return immediately.
s.sendingHdrAsync = true
streamHdr := s.streamHdr
s.streamHdr = nil
s.streamHdrMu.Unlock()
go func() {
s.SetWriteDeadline(time.Time{})
_, _ = s.str.Write(streamHdr)
s.str.SetReliableBoundary()
s.str.CancelWrite(webtransportCodeToHTTPCode(e))
s.onClose()
}()
return
}
s.streamHdrMu.Unlock()
s.str.CancelWrite(webtransportCodeToHTTPCode(e))
s.onClose()
}
func (s *SendStream) closeWithSession(err error) {
s.closeOnce.Do(func() {
s.closeErr = err
s.str.CancelWrite(WTSessionGoneErrorCode)
close(s.closed)
})
}
// Close closes the write-direction of the stream.
// Future calls to Write are not permitted after calling Close.
func (s *SendStream) Close() error {
// if a Goroutine is already sending the header, return immediately
s.streamHdrMu.Lock()
if s.sendingHdrAsync {
s.streamHdrMu.Unlock()
return nil
}
if len(s.streamHdr) > 0 {
// Sending the stream header might block if we are blocked by flow control.
// Send a stream header async so that CancelWrite can return immediately.
s.sendingHdrAsync = true
streamHdr := s.streamHdr
s.streamHdr = nil
s.streamHdrMu.Unlock()
go func() {
s.SetWriteDeadline(time.Time{})
_, _ = s.str.Write(streamHdr)
s.str.SetReliableBoundary()
_ = s.str.Close()
s.onClose()
}()
return nil
}
s.streamHdrMu.Unlock()
s.onClose()
return maybeConvertStreamError(s.str.Close())
}
// The Context is canceled as soon as the write-side of the stream is closed.
// This happens when Close() or CancelWrite() is called, or when the peer
// cancels the read-side of their stream.
// The cancellation cause is set to the error that caused the stream to
// close, or `context.Canceled` in case the stream is closed without error.
func (s *SendStream) Context() context.Context {
return s.str.Context()
}
// SetWriteDeadline sets the deadline for future Write calls
// and any currently-blocked Write call.
// Even if write times out, it may return n > 0, indicating that
// some data was successfully written.
// A zero value for t means Write will not time out.
func (s *SendStream) SetWriteDeadline(t time.Time) error {
s.deadlineMu.Lock()
s.writeDeadline = t
if s.deadlineNotifyCh != nil {
select {
case s.deadlineNotifyCh <- struct{}{}:
default:
}
}
s.deadlineMu.Unlock()
return maybeConvertStreamError(s.str.SetWriteDeadline(t))
}
// A ReceiveStream is a unidirectional WebTransport receive stream.
type ReceiveStream struct {
str quicReceiveStream
onClose func() // to remove the stream from the streamsMap
closeOnce sync.Once
closed chan struct{}
closeErr error
deadlineMu sync.Mutex
readDeadline time.Time
deadlineNotifyCh chan struct{} // receives a value when deadline changes
}
func newReceiveStream(str quicReceiveStream, onClose func()) *ReceiveStream {
return &ReceiveStream{
str: str,
closed: make(chan struct{}),
onClose: onClose,
}
}
// Read reads data from the stream.
// Read can be made to time out using [ReceiveStream.SetReadDeadline].
// If the stream was canceled, the error is a [StreamError].
func (s *ReceiveStream) Read(b []byte) (int, error) {
n, err := s.str.Read(b)
if err != nil && !isTimeoutError(err) {
s.onClose()
}
var strErr *quic.StreamError
if errors.As(err, &strErr) && strErr.ErrorCode == WTSessionGoneErrorCode {
return n, s.handleSessionGoneError()
}
return n, maybeConvertStreamError(err)
}
// handleSessionGoneError waits for the session to be closed after receiving a WTSessionGoneErrorCode.
// If the peer is initiating the session close, we might need to wait for the CONNECT stream to be closed.
// While a malicious peer might withhold the session close, this is not an interesting attack vector:
// 1. a WebTransport stream consumes very little memory, and
// 2. the number of concurrent WebTransport sessions is limited.
func (s *ReceiveStream) handleSessionGoneError() error {
s.deadlineMu.Lock()
if s.deadlineNotifyCh == nil {
s.deadlineNotifyCh = make(chan struct{}, 1)
}
s.deadlineMu.Unlock()
for {
s.deadlineMu.Lock()
deadline := s.readDeadline
s.deadlineMu.Unlock()
var timerCh <-chan time.Time
if !deadline.IsZero() {
if d := time.Until(deadline); d > 0 {
timerCh = time.After(d)
} else {
return os.ErrDeadlineExceeded
}
}
select {
case <-s.closed:
return s.closeErr
case <-timerCh:
return os.ErrDeadlineExceeded
case <-s.deadlineNotifyCh:
}
}
}
// CancelRead aborts receiving on this stream.
// It instructs the peer to stop transmitting stream data.
// Read will unblock immediately, and future Read calls will fail.
// When called multiple times it is a no-op.
func (s *ReceiveStream) CancelRead(e StreamErrorCode) {
s.str.CancelRead(webtransportCodeToHTTPCode(e))
s.onClose()
}
func (s *ReceiveStream) closeWithSession(err error) {
s.closeOnce.Do(func() {
s.closeErr = err
s.str.CancelRead(WTSessionGoneErrorCode)
close(s.closed)
})
}
// SetReadDeadline sets the deadline for future Read calls and
// any currently-blocked Read call.
// A zero value for t means Read will not time out.
func (s *ReceiveStream) SetReadDeadline(t time.Time) error {
s.deadlineMu.Lock()
s.readDeadline = t
if s.deadlineNotifyCh != nil {
select {
case s.deadlineNotifyCh <- struct{}{}:
default:
}
}
s.deadlineMu.Unlock()
return maybeConvertStreamError(s.str.SetReadDeadline(t))
}
// Stream is a bidirectional WebTransport stream.
type Stream struct {
sendStr *SendStream
recvStr *ReceiveStream
mx sync.Mutex
sendSideClosed, recvSideClosed bool
onClose func()
}
func newStream(str *quic.Stream, hdr []byte, onClose func()) *Stream {
s := &Stream{onClose: onClose}
s.sendStr = newSendStream(str, hdr, func() { s.registerClose(true) })
s.recvStr = newReceiveStream(str, func() { s.registerClose(false) })
return s
}
// Write writes data to the stream.
// Write can be made to time out using [Stream.SetWriteDeadline] or [Stream.SetDeadline].
// If the stream was canceled, the error is a [StreamError].
func (s *Stream) Write(b []byte) (int, error) {
return s.sendStr.Write(b)
}
// Read reads data from the stream.
// Read can be made to time out using [Stream.SetReadDeadline] and [Stream.SetDeadline].
// If the stream was canceled, the error is a [StreamError].
func (s *Stream) Read(b []byte) (int, error) {
return s.recvStr.Read(b)
}
// CancelWrite aborts sending on this stream.
// See [SendStream.CancelWrite] for more details.
func (s *Stream) CancelWrite(e StreamErrorCode) {
s.sendStr.CancelWrite(e)
}
// CancelRead aborts receiving on this stream.
// See [ReceiveStream.CancelRead] for more details.
func (s *Stream) CancelRead(e StreamErrorCode) {
s.recvStr.CancelRead(e)
}
// Close closes the send-direction of the stream.
// It does not close the receive-direction of the stream.
func (s *Stream) Close() error {
return s.sendStr.Close()
}
func (s *Stream) registerClose(isSendSide bool) {
s.mx.Lock()
if isSendSide {
s.sendSideClosed = true
} else {
s.recvSideClosed = true
}
isClosed := s.sendSideClosed && s.recvSideClosed
s.mx.Unlock()
if isClosed {
s.onClose()
}
}
func (s *Stream) closeWithSession(err error) {
s.sendStr.closeWithSession(err)
s.recvStr.closeWithSession(err)
}
// The Context is canceled as soon as the write-side of the stream is closed.
// See [SendStream.Context] for more details.
func (s *Stream) Context() context.Context {
return s.sendStr.Context()
}
// SetWriteDeadline sets the deadline for future Write calls.
// See [SendStream.SetWriteDeadline] for more details.
func (s *Stream) SetWriteDeadline(t time.Time) error {
return s.sendStr.SetWriteDeadline(t)
}
// SetReadDeadline sets the deadline for future Read calls.
// See [ReceiveStream.SetReadDeadline] for more details.
func (s *Stream) SetReadDeadline(t time.Time) error {
return s.recvStr.SetReadDeadline(t)
}
// SetDeadline sets the read and write deadlines associated with the stream.
// It is equivalent to calling both SetReadDeadline and SetWriteDeadline.
func (s *Stream) SetDeadline(t time.Time) error {
err1 := s.SetWriteDeadline(t)
err2 := s.SetReadDeadline(t)
return errors.Join(err1, err2)
}
func maybeConvertStreamError(err error) error {
if err == nil {
return nil
}
var streamErr *quic.StreamError
if errors.As(err, &streamErr) {
errorCode, cerr := httpCodeToWebtransportCode(streamErr.ErrorCode)
if cerr != nil {
return fmt.Errorf("stream reset, but failed to convert stream error %d: %w", streamErr.ErrorCode, cerr)
}
return &StreamError{
ErrorCode: errorCode,
Remote: streamErr.Remote,
}
}
return err
}
func isTimeoutError(err error) bool {
nerr, ok := err.(net.Error)
if !ok {
return false
}
return nerr.Timeout()
}

View File

@@ -0,0 +1,42 @@
package webtransport
import (
"sync"
"github.com/quic-go/quic-go"
)
type closeFunc func(error)
// The streamsMap manages the streams of a single QUIC connection.
// Note that several WebTransport sessions can share one QUIC connection.
type streamsMap struct {
mx sync.Mutex
m map[quic.StreamID]closeFunc
}
func newStreamsMap() *streamsMap {
return &streamsMap{m: make(map[quic.StreamID]closeFunc)}
}
func (s *streamsMap) AddStream(id quic.StreamID, close closeFunc) {
s.mx.Lock()
s.m[id] = close
s.mx.Unlock()
}
func (s *streamsMap) RemoveStream(id quic.StreamID) {
s.mx.Lock()
delete(s.m, id)
s.mx.Unlock()
}
func (s *streamsMap) CloseSession(err error) {
s.mx.Lock()
defer s.mx.Unlock()
for _, cl := range s.m {
cl(err)
}
s.m = nil
}