add vendor data
All checks were successful
Create and publish a Docker image 🚀 / build-and-push-image (push) Successful in 1m49s
All checks were successful
Create and publish a Docker image 🚀 / build-and-push-image (push) Successful in 1m49s
This commit is contained in:
2
vendor/github.com/quic-go/webtransport-go/.gitignore
generated
vendored
Normal file
2
vendor/github.com/quic-go/webtransport-go/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.qlog
|
||||
*.sqlog
|
||||
45
vendor/github.com/quic-go/webtransport-go/.golangci.yml
generated
vendored
Normal file
45
vendor/github.com/quic-go/webtransport-go/.golangci.yml
generated
vendored
Normal 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
7
vendor/github.com/quic-go/webtransport-go/LICENSE
generated
vendored
Normal 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
25
vendor/github.com/quic-go/webtransport-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# webtransport-go
|
||||
|
||||
[](https://quic-go.net/docs/)
|
||||
[](https://pkg.go.dev/github.com/quic-go/webtransport-go)
|
||||
[](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. |  |
|
||||
| [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 |  |
|
||||
| [signalr](https://github.com/philippseith/signalr) | SignalR server and client in Go |  |
|
||||
|
||||
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
14
vendor/github.com/quic-go/webtransport-go/SECURITY.md
generated
vendored
Normal 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
273
vendor/github.com/quic-go/webtransport-go/client.go
generated
vendored
Normal 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
10
vendor/github.com/quic-go/webtransport-go/codecov.yml
generated
vendored
Normal 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
76
vendor/github.com/quic-go/webtransport-go/errors.go
generated
vendored
Normal 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
|
||||
}
|
||||
5
vendor/github.com/quic-go/webtransport-go/protocol.go
generated
vendored
Normal file
5
vendor/github.com/quic-go/webtransport-go/protocol.go
generated
vendored
Normal 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
445
vendor/github.com/quic-go/webtransport-go/server.go
generated
vendored
Normal 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
467
vendor/github.com/quic-go/webtransport-go/session.go
generated
vendored
Normal 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]
|
||||
}
|
||||
179
vendor/github.com/quic-go/webtransport-go/session_manager.go
generated
vendored
Normal file
179
vendor/github.com/quic-go/webtransport-go/session_manager.go
generated
vendored
Normal 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
478
vendor/github.com/quic-go/webtransport-go/stream.go
generated
vendored
Normal 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()
|
||||
}
|
||||
42
vendor/github.com/quic-go/webtransport-go/streams_map.go
generated
vendored
Normal file
42
vendor/github.com/quic-go/webtransport-go/streams_map.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user