WIP: Save agent roles integration work before CHORUS rebrand

- Agent roles and coordination features
- Chat API integration testing
- New configuration and workspace management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-08-01 02:21:11 +10:00
parent 81b473d48f
commit 5978a0b8f5
3713 changed files with 1103925 additions and 59 deletions

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

@@ -0,0 +1 @@
qlog/

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.

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

@@ -0,0 +1,39 @@
# webtransport-go
[![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.
## Running a Server
```go
// create a new webtransport.Server, listening on (UDP) port 443
s := webtransport.Server{
H3: http3.Server{Addr: ":443"},
}
// Create a new HTTP endpoint /webtransport.
http.HandleFunc("/webtransport", func(w http.ResponseWriter, r *http.Request) {
conn, err := s.Upgrade(w, r)
if err != nil {
log.Printf("upgrading failed: %s", err)
w.WriteHeader(500)
return
}
// Handle the connection. Here goes the application logic.
})
s.ListenAndServeTLS(certFile, keyFile)
```
Now that the server is running, Chrome can be used to establish a new WebTransport session as described in [this tutorial](https://web.dev/webtransport/).
## Running a Client
```go
var d webtransport.Dialer
rsp, conn, err := d.Dial(ctx, "https://example.com/webtransport", nil)
// err is only nil if rsp.StatusCode is a 2xx
// Handle the connection. Here goes the application logic.
```

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

@@ -0,0 +1,124 @@
package webtransport
import (
"context"
"fmt"
"net/http"
"net/url"
"sync"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
)
type Dialer struct {
// If not set, reasonable defaults will be used.
// In order for WebTransport to function, this implementation will:
// * overwrite the StreamHijacker and UniStreamHijacker
// * enable datagram support
// * set the MaxIncomingStreams to 100 on the quic.Config, if unset
*http3.RoundTripper
// 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
ctx context.Context
ctxCancel context.CancelFunc
initOnce sync.Once
conns sessionManager
}
func (d *Dialer) init() {
timeout := d.StreamReorderingTimeout
if timeout == 0 {
timeout = 5 * time.Second
}
d.conns = *newSessionManager(timeout)
d.ctx, d.ctxCancel = context.WithCancel(context.Background())
if d.RoundTripper == nil {
d.RoundTripper = &http3.RoundTripper{}
}
d.RoundTripper.EnableDatagrams = true
if d.RoundTripper.AdditionalSettings == nil {
d.RoundTripper.AdditionalSettings = make(map[uint64]uint64)
}
d.RoundTripper.StreamHijacker = func(ft http3.FrameType, conn quic.Connection, str quic.Stream, e error) (hijacked bool, err error) {
if isWebTransportError(e) {
return true, nil
}
if ft != webTransportFrameType {
return false, nil
}
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if isWebTransportError(err) {
return true, nil
}
return false, err
}
d.conns.AddStream(conn, str, sessionID(id))
return true, nil
}
d.RoundTripper.UniStreamHijacker = func(st http3.StreamType, conn quic.Connection, str quic.ReceiveStream, err error) (hijacked bool) {
if st != webTransportUniStreamType && !isWebTransportError(err) {
return false
}
d.conns.AddUniStream(conn, str)
return true
}
if d.QuicConfig == nil {
d.QuicConfig = &quic.Config{}
}
if d.QuicConfig.MaxIncomingStreams == 0 {
d.QuicConfig.MaxIncomingStreams = 100
}
}
func (d *Dialer) Dial(ctx context.Context, urlStr string, reqHdr http.Header) (*http.Response, *Session, error) {
d.initOnce.Do(func() { d.init() })
u, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
if reqHdr == nil {
reqHdr = http.Header{}
}
reqHdr.Set(webTransportDraftOfferHeaderKey, "1")
req := &http.Request{
Method: http.MethodConnect,
Header: reqHdr,
Proto: "webtransport",
Host: u.Host,
URL: u,
}
req = req.WithContext(ctx)
rsp, err := d.RoundTripper.RoundTripOpt(req, http3.RoundTripOpt{DontCloseRequestStream: true})
if err != nil {
return nil, nil, err
}
if rsp.StatusCode < 200 || rsp.StatusCode >= 300 {
return rsp, nil, fmt.Errorf("received status %d", rsp.StatusCode)
}
str := rsp.Body.(http3.HTTPStreamer).HTTPStream()
conn := d.conns.AddSession(
rsp.Body.(http3.Hijacker).StreamCreator(),
sessionID(str.StreamID()),
str,
)
return rsp, conn, nil
}
func (d *Dialer) Close() error {
d.ctxCancel()
return nil
}

View File

@@ -0,0 +1,9 @@
comment: false
coverage:
status:
patch:
default:
informational: true
project:
default:
informational: true

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

@@ -0,0 +1,78 @@
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
}
func isWebTransportError(e error) bool {
if e == nil {
return false
}
var strErr *quic.StreamError
if !errors.As(e, &strErr) {
return false
}
if strErr.ErrorCode == sessionCloseErrorCode {
return true
}
_, err := httpCodeToWebtransportCode(strErr.ErrorCode)
return err == nil
}
// WebTransportBufferedStreamRejectedErrorCode is the error code of the
// H3_WEBTRANSPORT_BUFFERED_STREAM_REJECTED error.
const WebTransportBufferedStreamRejectedErrorCode quic.StreamErrorCode = 0x3994bd84
// StreamError is the error that is returned from stream operations (Read, Write) when the stream is canceled.
type StreamError struct {
ErrorCode StreamErrorCode
}
func (e *StreamError) Is(target error) bool {
_, ok := target.(*StreamError)
return ok
}
func (e *StreamError) Error() string {
return fmt.Sprintf("stream canceled with error code %d", e.ErrorCode)
}
// ConnectionError is a WebTransport connection error.
type ConnectionError struct {
Remote bool
ErrorCode SessionErrorCode
Message string
}
var _ error = &ConnectionError{}
func (e *ConnectionError) Error() string { return e.Message }

View File

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

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

@@ -0,0 +1,227 @@
package webtransport
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sync"
"time"
"unicode/utf8"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
)
const (
webTransportDraftOfferHeaderKey = "Sec-Webtransport-Http3-Draft02"
webTransportDraftHeaderKey = "Sec-Webtransport-Http3-Draft"
webTransportDraftHeaderValue = "draft02"
)
const (
webTransportFrameType = 0x41
webTransportUniStreamType = 0x54
)
type Server struct {
H3 http3.Server
// StreamReorderingTime is the time an incoming WebTransport stream that cannot be associated
// with a session is buffered.
// This can happen if the CONNECT request (that creates a new session) is reordered, and arrives
// after the first WebTransport stream(s) for that session.
// Defaults to 5 seconds.
StreamReorderingTimeout time.Duration
// 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
conns *sessionManager
}
func (s *Server) initialize() error {
s.initOnce.Do(func() {
s.initErr = s.init()
})
return s.initErr
}
func (s *Server) init() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
timeout := s.StreamReorderingTimeout
if timeout == 0 {
timeout = 5 * time.Second
}
s.conns = newSessionManager(timeout)
if s.CheckOrigin == nil {
s.CheckOrigin = checkSameOrigin
}
// configure the http3.Server
if s.H3.AdditionalSettings == nil {
s.H3.AdditionalSettings = make(map[uint64]uint64)
}
s.H3.AdditionalSettings[settingsEnableWebtransport] = 1
s.H3.EnableDatagrams = true
if s.H3.StreamHijacker != nil {
return errors.New("StreamHijacker already set")
}
s.H3.StreamHijacker = func(ft http3.FrameType, qconn quic.Connection, str quic.Stream, err error) (bool /* hijacked */, error) {
if isWebTransportError(err) {
return true, nil
}
if ft != webTransportFrameType {
return false, nil
}
// Reading the varint might block if the peer sends really small frames, but this is fine.
// This function is called from the HTTP/3 request handler, which runs in its own Go routine.
id, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if isWebTransportError(err) {
return true, nil
}
return false, err
}
s.conns.AddStream(qconn, str, sessionID(id))
return true, nil
}
s.H3.UniStreamHijacker = func(st http3.StreamType, qconn quic.Connection, str quic.ReceiveStream, err error) (hijacked bool) {
if st != webTransportUniStreamType && !isWebTransportError(err) {
return false
}
s.conns.AddUniStream(qconn, str)
return true
}
return nil
}
func (s *Server) Serve(conn net.PacketConn) error {
if err := s.initialize(); err != nil {
return err
}
return s.H3.Serve(conn)
}
// ServeQUICConn serves a single QUIC connection.
func (s *Server) ServeQUICConn(conn quic.Connection) error {
if err := s.initialize(); err != nil {
return err
}
return s.H3.ServeQUICConn(conn)
}
func (s *Server) ListenAndServe() error {
if err := s.initialize(); err != nil {
return err
}
return s.H3.ListenAndServe()
}
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
if err := s.initialize(); err != nil {
return err
}
return s.H3.ListenAndServeTLS(certFile, keyFile)
}
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()
}
if s.conns != nil {
s.conns.Close()
}
err := s.H3.Close()
s.refCount.Wait()
return err
}
func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*Session, error) {
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 v, ok := r.Header[webTransportDraftOfferHeaderKey]; !ok || len(v) != 1 || v[0] != "1" {
return nil, fmt.Errorf("missing or invalid %s header", webTransportDraftOfferHeaderKey)
}
if !s.CheckOrigin(r) {
return nil, errors.New("webtransport: request origin not allowed")
}
w.Header().Add(webTransportDraftHeaderKey, webTransportDraftHeaderValue)
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
httpStreamer, ok := r.Body.(http3.HTTPStreamer)
if !ok { // should never happen, unless quic-go changed the API
return nil, errors.New("failed to take over HTTP stream")
}
str := httpStreamer.HTTPStream()
sID := sessionID(str.StreamID())
hijacker, ok := w.(http3.Hijacker)
if !ok { // should never happen, unless quic-go changed the API
return nil, errors.New("failed to hijack")
}
return s.conns.AddSession(
hijacker.StreamCreator(),
sID,
r.Body.(http3.HTTPStreamer).HTTPStream(),
), nil
}
// 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
}

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

@@ -0,0 +1,418 @@
package webtransport
import (
"context"
"encoding/binary"
"errors"
"io"
"math/rand"
"net"
"sync"
"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 closeWebtransportSessionCapsuleType http3.CapsuleType = 0x2843
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 Session struct {
sessionID sessionID
qconn http3.StreamCreator
requestStr quic.Stream
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]
// TODO: garbage collect streams from when they are closed
streams streamsMap
}
func newSession(sessionID sessionID, qconn http3.StreamCreator, requestStr quic.Stream) *Session {
tracingID := qconn.Context().Value(quic.ConnectionTracingKey).(uint64)
ctx, ctxCancel := context.WithCancel(context.WithValue(context.Background(), quic.ConnectionTracingKey, tracingID))
c := &Session{
sessionID: sessionID,
qconn: qconn,
requestStr: requestStr,
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() {
var closeErr *ConnectionError
err := s.parseNextCapsule()
if !errors.As(err, &closeErr) {
closeErr = &ConnectionError{Remote: true}
}
s.closeMx.Lock()
defer s.closeMx.Unlock()
// If we closed the connection, the closeErr will be set in Close.
if s.closeErr == nil {
s.closeErr = closeErr
}
for _, cancel := range s.streamCtxs {
cancel()
}
s.streams.CloseSession()
}
// parseNextCapsule parses the next Capsule sent on the request stream.
// It returns a ConnectionError, if the capsule received is a CLOSE_WEBTRANSPORT_SESSION Capsule.
func (s *Session) parseNextCapsule() error {
for {
// TODO: enforce max size
typ, r, err := http3.ParseCapsule(quicvarint.NewReader(s.requestStr))
if err != nil {
return err
}
switch typ {
case closeWebtransportSessionCapsuleType:
b := make([]byte, 4)
if _, err := io.ReadFull(r, b); err != nil {
return err
}
appErrCode := binary.BigEndian.Uint32(b)
appErrMsg, err := io.ReadAll(r)
if err != nil {
return err
}
return &ConnectionError{
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(), func() {
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(sessionCloseErrorCode)
qstr.CancelWrite(sessionCloseErrorCode)
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(sessionCloseErrorCode)
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.qconn.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()
qstr, err := s.qconn.OpenStreamSync(ctx)
if err != nil {
if s.closeErr != nil {
return nil, s.closeErr
}
return nil, err
}
s.closeMx.Lock()
defer s.closeMx.Unlock()
delete(s.streamCtxs, id)
// Some time might have passed. Check if the session is still alive
if s.closeErr != nil {
qstr.CancelWrite(sessionCloseErrorCode)
qstr.CancelRead(sessionCloseErrorCode)
return nil, s.closeErr
}
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.qconn.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()
qstr, err := s.qconn.OpenUniStreamSync(ctx)
if err != nil {
if s.closeErr != nil {
return nil, s.closeErr
}
return nil, err
}
s.closeMx.Lock()
defer s.closeMx.Unlock()
delete(s.streamCtxs, id)
// Some time might have passed. Check if the session is still alive
if s.closeErr != nil {
qstr.CancelWrite(sessionCloseErrorCode)
return nil, s.closeErr
}
return s.addSendStream(qstr), nil
}
func (s *Session) LocalAddr() net.Addr {
return s.qconn.LocalAddr()
}
func (s *Session) RemoteAddr() net.Addr {
return s.qconn.RemoteAddr()
}
func (s *Session) CloseWithError(code SessionErrorCode, msg string) error {
first, err := s.closeWithError(code, msg)
if err != nil || !first {
return err
}
s.requestStr.CancelRead(1337)
err = s.requestStr.Close()
<-s.ctx.Done()
return err
}
func (s *Session) closeWithError(code SessionErrorCode, msg string) (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 = &ConnectionError{
ErrorCode: code,
Message: msg,
}
b := make([]byte, 4, 4+len(msg))
binary.BigEndian.PutUint32(b, uint32(code))
b = append(b, []byte(msg)...)
return true, http3.WriteCapsule(
quicvarint.NewWriter(s.requestStr),
closeWebtransportSessionCapsuleType,
b,
)
}
func (c *Session) ConnectionState() quic.ConnectionState {
return c.qconn.ConnectionState()
}

View File

@@ -0,0 +1,195 @@
package webtransport
import (
"context"
"sync"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
)
// session is the map value in the conns map
type session struct {
created chan struct{} // is closed once the session map has been initialized
counter int // how many streams are waiting for this session to be established
conn *Session
}
type sessionManager struct {
refCount sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc
timeout time.Duration
mx sync.Mutex
conns map[http3.StreamCreator]map[sessionID]*session
}
func newSessionManager(timeout time.Duration) *sessionManager {
m := &sessionManager{
timeout: timeout,
conns: make(map[http3.StreamCreator]map[sessionID]*session),
}
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
return m
}
// AddStream adds a new bidirectional stream to a WebTransport session.
// If the WebTransport session has not yet been established,
// it starts a new go routine and waits for establishment of the session.
// If that takes longer than timeout, the stream is reset.
func (m *sessionManager) AddStream(qconn http3.StreamCreator, str quic.Stream, id sessionID) {
sess, isExisting := m.getOrCreateSession(qconn, id)
if isExisting {
sess.conn.addIncomingStream(str)
return
}
m.refCount.Add(1)
go func() {
defer m.refCount.Done()
m.handleStream(str, sess)
m.mx.Lock()
defer m.mx.Unlock()
sess.counter--
// Once no more streams are waiting for this session to be established,
// and this session is still outstanding, delete it from the map.
if sess.counter == 0 && sess.conn == nil {
m.maybeDelete(qconn, id)
}
}()
}
func (m *sessionManager) maybeDelete(qconn http3.StreamCreator, id sessionID) {
sessions, ok := m.conns[qconn]
if !ok { // should never happen
return
}
delete(sessions, id)
if len(sessions) == 0 {
delete(m.conns, qconn)
}
}
// AddUniStream adds a new unidirectional stream to a WebTransport session.
// If the WebTransport session has not yet been established,
// it starts a new go routine and waits for establishment of the session.
// If that takes longer than timeout, the stream is reset.
func (m *sessionManager) AddUniStream(qconn http3.StreamCreator, str quic.ReceiveStream) {
idv, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
str.CancelRead(1337)
}
id := sessionID(idv)
sess, isExisting := m.getOrCreateSession(qconn, id)
if isExisting {
sess.conn.addIncomingUniStream(str)
return
}
m.refCount.Add(1)
go func() {
defer m.refCount.Done()
m.handleUniStream(str, sess)
m.mx.Lock()
defer m.mx.Unlock()
sess.counter--
// Once no more streams are waiting for this session to be established,
// and this session is still outstanding, delete it from the map.
if sess.counter == 0 && sess.conn == nil {
m.maybeDelete(qconn, id)
}
}()
}
func (m *sessionManager) getOrCreateSession(qconn http3.StreamCreator, id sessionID) (sess *session, existed bool) {
m.mx.Lock()
defer m.mx.Unlock()
sessions, ok := m.conns[qconn]
if !ok {
sessions = make(map[sessionID]*session)
m.conns[qconn] = sessions
}
sess, ok = sessions[id]
if ok && sess.conn != nil {
return sess, true
}
if !ok {
sess = &session{created: make(chan struct{})}
sessions[id] = sess
}
sess.counter++
return sess, false
}
func (m *sessionManager) handleStream(str quic.Stream, sess *session) {
t := time.NewTimer(m.timeout)
defer t.Stop()
// When multiple streams are waiting for the same session to be established,
// the timeout is calculated for every stream separately.
select {
case <-sess.created:
sess.conn.addIncomingStream(str)
case <-t.C:
str.CancelRead(WebTransportBufferedStreamRejectedErrorCode)
str.CancelWrite(WebTransportBufferedStreamRejectedErrorCode)
case <-m.ctx.Done():
}
}
func (m *sessionManager) handleUniStream(str quic.ReceiveStream, sess *session) {
t := time.NewTimer(m.timeout)
defer t.Stop()
// When multiple streams are waiting for the same session to be established,
// the timeout is calculated for every stream separately.
select {
case <-sess.created:
sess.conn.addIncomingUniStream(str)
case <-t.C:
str.CancelRead(WebTransportBufferedStreamRejectedErrorCode)
case <-m.ctx.Done():
}
}
// AddSession adds a new WebTransport session.
func (m *sessionManager) AddSession(qconn http3.StreamCreator, id sessionID, requestStr quic.Stream) *Session {
conn := newSession(id, qconn, requestStr)
m.mx.Lock()
defer m.mx.Unlock()
sessions, ok := m.conns[qconn]
if !ok {
sessions = make(map[sessionID]*session)
m.conns[qconn] = sessions
}
if sess, ok := sessions[id]; ok {
// We might already have an entry of this session.
// This can happen when we receive a stream for this WebTransport session before we complete the HTTP request
// that establishes the session.
sess.conn = conn
close(sess.created)
return conn
}
c := make(chan struct{})
close(c)
sessions[id] = &session{created: c, conn: conn}
return conn
}
func (m *sessionManager) Close() {
m.ctxCancel()
m.refCount.Wait()
}

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

@@ -0,0 +1,214 @@
package webtransport
import (
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/quic-go/quic-go"
)
const sessionCloseErrorCode quic.StreamErrorCode = 0x170d7b68
type SendStream interface {
io.Writer
io.Closer
StreamID() quic.StreamID
CancelWrite(StreamErrorCode)
SetWriteDeadline(time.Time) error
}
type ReceiveStream interface {
io.Reader
StreamID() quic.StreamID
CancelRead(StreamErrorCode)
SetReadDeadline(time.Time) error
}
type Stream interface {
SendStream
ReceiveStream
SetDeadline(time.Time) error
}
type sendStream struct {
str quic.SendStream
// 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
onClose func()
once sync.Once
}
var _ SendStream = &sendStream{}
func newSendStream(str quic.SendStream, hdr []byte, onClose func()) *sendStream {
return &sendStream{str: str, streamHdr: hdr, onClose: onClose}
}
func (s *sendStream) maybeSendStreamHeader() (err error) {
s.once.Do(func() {
if _, e := s.str.Write(s.streamHdr); e != nil {
err = e
return
}
s.streamHdr = nil
})
return
}
func (s *sendStream) Write(b []byte) (int, error) {
if err := s.maybeSendStreamHeader(); err != nil {
return 0, err
}
n, err := s.str.Write(b)
if err != nil && !isTimeoutError(err) {
s.onClose()
}
return n, maybeConvertStreamError(err)
}
func (s *sendStream) CancelWrite(e StreamErrorCode) {
s.str.CancelWrite(webtransportCodeToHTTPCode(e))
s.onClose()
}
func (s *sendStream) closeWithSession() {
s.str.CancelWrite(sessionCloseErrorCode)
}
func (s *sendStream) Close() error {
if err := s.maybeSendStreamHeader(); err != nil {
return err
}
s.onClose()
return maybeConvertStreamError(s.str.Close())
}
func (s *sendStream) SetWriteDeadline(t time.Time) error {
return maybeConvertStreamError(s.str.SetWriteDeadline(t))
}
func (s *sendStream) StreamID() quic.StreamID {
return s.str.StreamID()
}
type receiveStream struct {
str quic.ReceiveStream
onClose func()
}
var _ ReceiveStream = &receiveStream{}
func newReceiveStream(str quic.ReceiveStream, onClose func()) *receiveStream {
return &receiveStream{str: str, onClose: onClose}
}
func (s *receiveStream) Read(b []byte) (int, error) {
n, err := s.str.Read(b)
if err != nil && !isTimeoutError(err) {
s.onClose()
}
return n, maybeConvertStreamError(err)
}
func (s *receiveStream) CancelRead(e StreamErrorCode) {
s.str.CancelRead(webtransportCodeToHTTPCode(e))
s.onClose()
}
func (s *receiveStream) closeWithSession() {
s.str.CancelRead(sessionCloseErrorCode)
}
func (s *receiveStream) SetReadDeadline(t time.Time) error {
return maybeConvertStreamError(s.str.SetReadDeadline(t))
}
func (s *receiveStream) StreamID() quic.StreamID {
return s.str.StreamID()
}
type stream struct {
*sendStream
*receiveStream
mx sync.Mutex
sendSideClosed, recvSideClosed bool
onClose func()
}
var _ Stream = &stream{}
func newStream(str quic.Stream, hdr []byte, onClose func()) *stream {
s := &stream{onClose: onClose}
s.sendStream = newSendStream(str, hdr, func() { s.registerClose(true) })
s.receiveStream = newReceiveStream(str, func() { s.registerClose(false) })
return s
}
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() {
s.sendStream.closeWithSession()
s.receiveStream.closeWithSession()
}
func (s *stream) SetDeadline(t time.Time) error {
err1 := s.sendStream.SetWriteDeadline(t)
err2 := s.receiveStream.SetReadDeadline(t)
if err1 != nil {
return err1
}
return err2
}
func (s *stream) StreamID() quic.StreamID {
return s.receiveStream.StreamID()
}
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}
}
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()
// 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() {
s.mx.Lock()
defer s.mx.Unlock()
for _, cl := range s.m {
cl()
}
s.m = nil
}

View File

@@ -0,0 +1,3 @@
{
"version": "v0.6.0"
}