├── mux ├── doc.go ├── frame │ ├── frame.go │ ├── message.go │ ├── encoder.go │ ├── message_eof.go │ ├── message_close.go │ ├── message_openfailure.go │ ├── message_open.go │ ├── message_windowadjust.go │ ├── message_data.go │ ├── message_openconfirm.go │ ├── frame_test.go │ └── decoder.go ├── dial_io.go ├── listen.go ├── dial_ws.go ├── dial_net.go ├── proxy.go ├── listen_io.go ├── listen_net.go ├── util_chanlist.go ├── listen_ws.go ├── util_window.go ├── util_buffer.go ├── proxy_test.go ├── transport_test.go ├── session_test.go ├── session.go └── channel.go ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── x ├── cbor │ ├── cbor.go │ ├── mux │ │ ├── frame.go │ │ ├── buffer.go │ │ ├── channel.go │ │ ├── session_test.go │ │ └── session.go │ ├── go.mod │ ├── codec │ │ └── cbor.go │ ├── _example │ │ └── main.go │ └── go.sum ├── quic │ ├── quic.go │ └── quic_test.go └── webrtc │ ├── _example │ └── main.go │ └── webrtc.go ├── qtalk.go ├── .gitignore ├── codec ├── json.go ├── codec.go └── codec_test.go ├── cmd └── qtalk │ ├── main.go │ ├── call.go │ ├── cli │ ├── context.go │ ├── framework.go │ ├── cli_test.go │ ├── help.go │ └── command.go │ ├── interop.go │ ├── bench.go │ └── check.go ├── rpc ├── rpctest │ └── rpctest.go ├── proxy.go ├── proxy_test.go ├── frame.go ├── server.go ├── client.go ├── rpc.go ├── handler.go └── rpc_test.go ├── talk ├── peer.go ├── dial.go └── peer_test.go ├── LICENSE ├── interop ├── callbacks.go └── service.go ├── go.mod ├── README.md ├── exp ├── ptr_test.go └── ptr.go ├── fn ├── call.go ├── call_test.go ├── handler.go └── handler_test.go └── go.sum /mux/doc.go: -------------------------------------------------------------------------------- 1 | // Package mux implements a qmux session and channel API. 2 | package mux 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [progrium] 4 | -------------------------------------------------------------------------------- /x/cbor/cbor.go: -------------------------------------------------------------------------------- 1 | package cbor 2 | 3 | import ( 4 | _ "github.com/progrium/qtalk-go/x/cbor/codec" 5 | _ "github.com/progrium/qtalk-go/x/cbor/mux" 6 | ) 7 | -------------------------------------------------------------------------------- /mux/frame/frame.go: -------------------------------------------------------------------------------- 1 | // Package frame implements encoding and decoding of qmux message frames. 2 | package frame 3 | 4 | import "io" 5 | 6 | var ( 7 | // Debug can be set to get message frames as they're encoded and decoded 8 | Debug io.Writer 9 | ) 10 | -------------------------------------------------------------------------------- /qtalk.go: -------------------------------------------------------------------------------- 1 | package qtalk 2 | 3 | import ( 4 | _ "github.com/progrium/qtalk-go/codec" 5 | _ "github.com/progrium/qtalk-go/fn" 6 | _ "github.com/progrium/qtalk-go/mux" 7 | _ "github.com/progrium/qtalk-go/rpc" 8 | _ "github.com/progrium/qtalk-go/talk" 9 | ) 10 | -------------------------------------------------------------------------------- /mux/frame/message.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | const ( 4 | msgChannelOpen = iota + 100 5 | msgChannelOpenConfirm 6 | msgChannelOpenFailure 7 | msgChannelWindowAdjust 8 | msgChannelData 9 | msgChannelEOF 10 | msgChannelClose 11 | ) 12 | 13 | type Message interface { 14 | Channel() (uint32, bool) 15 | String() string 16 | Bytes() []byte 17 | } 18 | -------------------------------------------------------------------------------- /x/cbor/mux/frame.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | const ( 4 | channelOpen = iota + 100 5 | channelOpenConfirm 6 | channelOpenFailure 7 | channelData 8 | channelEOF 9 | channelClose 10 | ) 11 | 12 | type Frame struct { 13 | _ struct{} `cbor:",toarray"` 14 | Type byte 15 | ChannelID uint32 16 | SenderID uint32 17 | Data []byte `cbor:",omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | TODO 17 | local 18 | _local 19 | cmd/qtalk/qtalk 20 | go.work -------------------------------------------------------------------------------- /x/cbor/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/progrium/qtalk-go/x/cbor 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fxamacker/cbor/v2 v2.4.0 // indirect 7 | github.com/mitchellh/mapstructure v1.4.3 // indirect 8 | github.com/progrium/qtalk-go v0.5.1-0.20230305191028-54c22354090f // indirect 9 | github.com/x448/float16 v0.8.4 // indirect 10 | golang.org/x/net v0.0.0-20210420210106-798c2154c571 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /mux/dial_io.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // DialIO establishes a mux session using a WriterCloser and ReadCloser. 9 | func DialIO(out io.WriteCloser, in io.ReadCloser) (Session, error) { 10 | return New(&ioduplex{out, in}), nil 11 | } 12 | 13 | // DialIO establishes a mux session using Stdout and Stdin. 14 | func DialStdio() (Session, error) { 15 | return DialIO(os.Stdout, os.Stdin) 16 | } 17 | -------------------------------------------------------------------------------- /codec/json.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | // JSONCodec provides a codec API for the standard library JSON encoder and decoder. 9 | type JSONCodec struct{} 10 | 11 | // Encoder returns a JSON encoder 12 | func (c JSONCodec) Encoder(w io.Writer) Encoder { 13 | return json.NewEncoder(w) 14 | } 15 | 16 | // Decoder returns a JSON decoder 17 | func (c JSONCodec) Decoder(r io.Reader) Decoder { 18 | return json.NewDecoder(r) 19 | } 20 | -------------------------------------------------------------------------------- /mux/listen.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import "net" 4 | 5 | // A Listener is similar to a net.Listener but returns connections wrapped as mux sessions. 6 | type Listener interface { 7 | // Close closes the listener. 8 | // Any blocked Accept operations will be unblocked and return errors. 9 | Close() error 10 | 11 | // Accept waits for and returns the next incoming session. 12 | Accept() (Session, error) 13 | 14 | // Addr returns the listener's network address if available. 15 | Addr() net.Addr 16 | } 17 | -------------------------------------------------------------------------------- /x/cbor/codec/cbor.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/fxamacker/cbor/v2" 7 | "github.com/progrium/qtalk-go/codec" 8 | ) 9 | 10 | // CBORCodec provides a codec API for a CBOR encoder and decoder. 11 | type CBORCodec struct{} 12 | 13 | // Encoder returns a CBOR encoder 14 | func (c CBORCodec) Encoder(w io.Writer) codec.Encoder { 15 | return cbor.NewEncoder(w) 16 | } 17 | 18 | // Decoder returns a CBOR decoder 19 | func (c CBORCodec) Decoder(r io.Reader) codec.Decoder { 20 | return cbor.NewDecoder(r) 21 | } 22 | -------------------------------------------------------------------------------- /codec/codec.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Encoder interface { 8 | // Encode writes an encoding of v to its Writer. 9 | Encode(v interface{}) error 10 | } 11 | 12 | type Decoder interface { 13 | // Decode reads the next encoded value from its Reader and stores it in the value pointed to by v. 14 | Decode(v interface{}) error 15 | } 16 | 17 | // Codec returns an Encoder or Decoder given a Writer or Reader. 18 | type Codec interface { 19 | Encoder(w io.Writer) Encoder 20 | Decoder(r io.Reader) Decoder 21 | } 22 | -------------------------------------------------------------------------------- /mux/frame/encoder.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // Encoder encodes messages given an io.Writer 10 | type Encoder struct { 11 | w io.Writer 12 | sync.Mutex 13 | } 14 | 15 | func NewEncoder(w io.Writer) *Encoder { 16 | return &Encoder{w: w} 17 | } 18 | 19 | func (enc *Encoder) Encode(msg Message) error { 20 | enc.Lock() 21 | defer enc.Unlock() 22 | 23 | if Debug != nil { 24 | fmt.Fprintln(Debug, "< 1 { 30 | sargs, err = clon.Parse(args[1:]) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | 36 | peer, err := talk.Dial(u.Scheme, u.Host, codec.JSONCodec{}) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | defer peer.Close() 41 | 42 | var ret any 43 | _, err = peer.Call(context.Background(), u.Path, sargs, &ret) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | b, err := json.MarshalIndent(ret, "", " ") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | fmt.Println(string(b)) 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /mux/listen_io.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "os" 7 | ) 8 | 9 | // ioListener wraps a single ReadWriteCloser to use as a listener. 10 | type ioListener struct { 11 | io.ReadWriteCloser 12 | } 13 | 14 | // Accept will always return the wrapped ReadWriteCloser as a mux session. 15 | func (l *ioListener) Accept() (Session, error) { 16 | return New(l.ReadWriteCloser), nil 17 | } 18 | 19 | func (l *ioListener) Addr() net.Addr { 20 | return nil 21 | } 22 | 23 | type ioduplex struct { 24 | io.WriteCloser 25 | io.ReadCloser 26 | } 27 | 28 | func (d *ioduplex) Close() error { 29 | if err := d.WriteCloser.Close(); err != nil { 30 | return err 31 | } 32 | if err := d.ReadCloser.Close(); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | // ListenIO returns an IOListener that gives a mux session based on seperate 39 | // WriteCloser and ReadClosers. 40 | func ListenIO(out io.WriteCloser, in io.ReadCloser) (Listener, error) { 41 | return &ioListener{ 42 | &ioduplex{out, in}, 43 | }, nil 44 | } 45 | 46 | // ListenStdio is a convenience for calling ListenIO with Stdout and Stdin. 47 | func ListenStdio() (Listener, error) { 48 | return ListenIO(os.Stdout, os.Stdin) 49 | } 50 | -------------------------------------------------------------------------------- /talk/dial.go: -------------------------------------------------------------------------------- 1 | package talk 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/progrium/qtalk-go/codec" 7 | "github.com/progrium/qtalk-go/mux" 8 | ) 9 | 10 | // A Dialer connects to address and establishes a mux.Session 11 | type Dialer func(addr string) (mux.Session, error) 12 | 13 | // Dialers is map of transport strings to Dialers 14 | // and includes all builtin transports 15 | var Dialers map[string]Dialer 16 | 17 | func init() { 18 | Dialers = map[string]Dialer{ 19 | "tcp": mux.DialTCP, 20 | "unix": mux.DialUnix, 21 | "ws": mux.DialWS, 22 | "stdio": func(_ string) (mux.Session, error) { 23 | return mux.DialStdio() 24 | }, 25 | } 26 | } 27 | 28 | // Dial connects to a remote address using a registered transport and returns a Peer. 29 | // Available transports are "tcp", "unix", "ws", and "stdio". In the case of "stdio", 30 | // the addr can be left an empty string. 31 | func Dial(transport, addr string, codec codec.Codec) (*Peer, error) { 32 | d, ok := Dialers[transport] 33 | if !ok { 34 | return nil, fmt.Errorf("transport '%s' not in available in Dialers", transport) 35 | } 36 | sess, err := d(addr) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return NewPeer(sess, codec), nil 41 | } 42 | -------------------------------------------------------------------------------- /interop/callbacks.go: -------------------------------------------------------------------------------- 1 | package interop 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "github.com/progrium/qtalk-go/rpc" 8 | ) 9 | 10 | type CallbackService struct{} 11 | 12 | func (s CallbackService) Unary(resp rpc.Responder, call *rpc.Call) { 13 | var params any 14 | if err := call.Receive(¶ms); err != nil { 15 | log.Println(err) 16 | return 17 | } 18 | if err := resp.Return(params); err != nil { 19 | log.Println(err) 20 | } 21 | } 22 | 23 | func (s CallbackService) Stream(resp rpc.Responder, call *rpc.Call) { 24 | var v any 25 | if err := call.Receive(&v); err != nil { 26 | log.Println(err) 27 | return 28 | } 29 | ch, err := resp.Continue(v) 30 | if err != nil { 31 | log.Println(err) 32 | return 33 | } 34 | defer ch.Close() 35 | for err == nil { 36 | err = call.Receive(&v) 37 | if err == nil { 38 | err = resp.Send(v) 39 | } 40 | } 41 | } 42 | 43 | func (s CallbackService) Bytes(resp rpc.Responder, call *rpc.Call) { 44 | var params any 45 | if err := call.Receive(¶ms); err != nil { 46 | log.Println(err) 47 | return 48 | } 49 | ch, err := resp.Continue(params) 50 | if err != nil { 51 | log.Println(err) 52 | return 53 | } 54 | defer ch.Close() 55 | io.Copy(ch, call) 56 | } 57 | -------------------------------------------------------------------------------- /talk/peer_test.go: -------------------------------------------------------------------------------- 1 | package talk 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/progrium/qtalk-go/codec" 9 | "github.com/progrium/qtalk-go/mux" 10 | "github.com/progrium/qtalk-go/rpc" 11 | ) 12 | 13 | func TestPeerBidirectional(t *testing.T) { 14 | ar, bw := io.Pipe() 15 | br, aw := io.Pipe() 16 | sessA, _ := mux.DialIO(aw, ar) 17 | sessB, _ := mux.DialIO(bw, br) 18 | 19 | peerA := NewPeer(sessA, codec.JSONCodec{}) 20 | peerB := NewPeer(sessB, codec.JSONCodec{}) 21 | defer peerA.Close() 22 | defer peerB.Close() 23 | 24 | peerA.Handle("hello", rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) { 25 | r.Return("A") 26 | })) 27 | peerB.Handle("hello", rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) { 28 | r.Return("B") 29 | })) 30 | 31 | go peerA.Respond() 32 | go peerB.Respond() 33 | 34 | var retB string 35 | _, err := peerA.Call(context.Background(), "hello", nil, &retB) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if retB != "B" { 40 | t.Fatal("unexpected return:", retB) 41 | } 42 | 43 | var retA string 44 | _, err = peerB.Call(context.Background(), "hello", nil, &retA) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if retA != "A" { 49 | t.Fatal("unexpected return:", retA) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/qtalk/cli/context.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // ContextWithIO returns a child context with a ContextIO 9 | // value added using the given Stdio equivalents. 10 | func ContextWithIO(parent context.Context, in io.Reader, out io.Writer, err io.Writer) context.Context { 11 | return context.WithValue(parent, IOContextKey, &iocontext{ 12 | in: in, 13 | out: out, 14 | err: err, 15 | }) 16 | } 17 | 18 | type iocontext struct { 19 | out, err io.Writer 20 | in io.Reader 21 | } 22 | 23 | func (c *iocontext) Write(p []byte) (n int, err error) { 24 | return c.out.Write(p) 25 | } 26 | 27 | func (c *iocontext) Read(p []byte) (n int, err error) { 28 | return c.in.Read(p) 29 | } 30 | 31 | func (c *iocontext) Err() io.Writer { 32 | return c.err 33 | } 34 | 35 | // ContextIO is an io.ReadWriter with an extra io.Writer 36 | // for an error channel. Typically wrapping STDIO. 37 | type ContextIO interface { 38 | io.Reader 39 | io.Writer 40 | Err() io.Writer 41 | } 42 | 43 | // IOContextKey is the context key used by IOFrom to get a ContextIO. 44 | var IOContextKey = "io" 45 | 46 | // IOFrom pulls a ContextIO from a context. 47 | func IOFrom(ctx context.Context) ContextIO { 48 | v := ctx.Value(IOContextKey) 49 | if v == nil { 50 | return nil 51 | } 52 | return v.(ContextIO) 53 | } 54 | -------------------------------------------------------------------------------- /mux/listen_net.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // netListener wraps a net.Listener to return connected mux sessions. 8 | type netListener struct { 9 | net.Listener 10 | } 11 | 12 | // Accept waits for and returns the next connected session to the listener. 13 | func (l *netListener) Accept() (Session, error) { 14 | conn, err := l.Listener.Accept() 15 | if err != nil { 16 | return nil, err 17 | } 18 | return New(conn), nil 19 | } 20 | 21 | // Close closes the listener. 22 | // Any blocked Accept operations will be unblocked and return errors. 23 | func (l *netListener) Close() error { 24 | return l.Listener.Close() 25 | } 26 | 27 | func (l *netListener) Addr() net.Addr { 28 | return l.Listener.Addr() 29 | } 30 | 31 | func ListenerFrom(l net.Listener) Listener { 32 | return &netListener{Listener: l} 33 | } 34 | 35 | // ListenTCP creates a TCP listener at the given address. 36 | func ListenTCP(addr string) (Listener, error) { 37 | l, err := net.Listen("tcp", addr) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return ListenerFrom(l), nil 42 | } 43 | 44 | // ListenTCP creates a Unix domain socket listener at the given path. 45 | func ListenUnix(path string) (Listener, error) { 46 | l, err := net.Listen("unix", path) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return ListenerFrom(l), nil 51 | } 52 | -------------------------------------------------------------------------------- /mux/util_chanlist.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import "sync" 4 | 5 | // chanList is a thread safe channel list. 6 | type chanList struct { 7 | // protects concurrent access to chans 8 | sync.Mutex 9 | 10 | // chans are indexed by the local id of the channel, which the 11 | // other side should send in the PeersId field. 12 | chans []*channel 13 | } 14 | 15 | // Assigns a channel ID to the given channel. 16 | func (c *chanList) add(ch *channel) uint32 { 17 | c.Lock() 18 | defer c.Unlock() 19 | for i := range c.chans { 20 | if c.chans[i] == nil { 21 | c.chans[i] = ch 22 | return uint32(i) 23 | } 24 | } 25 | c.chans = append(c.chans, ch) 26 | return uint32(len(c.chans) - 1) 27 | } 28 | 29 | // getChan returns the channel for the given ID. 30 | func (c *chanList) getChan(id uint32) *channel { 31 | c.Lock() 32 | defer c.Unlock() 33 | if id < uint32(len(c.chans)) { 34 | return c.chans[id] 35 | } 36 | return nil 37 | } 38 | 39 | func (c *chanList) remove(id uint32) { 40 | c.Lock() 41 | if id < uint32(len(c.chans)) { 42 | c.chans[id] = nil 43 | } 44 | c.Unlock() 45 | } 46 | 47 | // dropAll forgets all channels it knows, returning them in a slice. 48 | func (c *chanList) dropAll() []*channel { 49 | c.Lock() 50 | defer c.Unlock() 51 | var r []*channel 52 | 53 | for _, ch := range c.chans { 54 | if ch == nil { 55 | continue 56 | } 57 | r = append(r, ch) 58 | } 59 | c.chans = nil 60 | return r 61 | } 62 | -------------------------------------------------------------------------------- /mux/listen_ws.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | 8 | "golang.org/x/net/websocket" 9 | ) 10 | 11 | // wsListener wraps a net.Listener and WebSocket server to return connected mux sessions. 12 | type wsListener struct { 13 | net.Listener 14 | accepted chan Session 15 | } 16 | 17 | // Accept waits for and returns the next connected session to the listener. 18 | func (l *wsListener) Accept() (Session, error) { 19 | sess, ok := <-l.accepted 20 | if !ok { 21 | return nil, io.EOF 22 | } 23 | return sess, nil 24 | } 25 | 26 | // Close closes the listener. 27 | // Any blocked Accept operations will be unblocked and return errors. 28 | func (l *wsListener) Close() error { 29 | close(l.accepted) 30 | return l.Listener.Close() 31 | } 32 | 33 | func (l *wsListener) Addr() net.Addr { 34 | return l.Listener.Addr() 35 | } 36 | 37 | // ListenWS takes a TCP address and returns a Listener for a HTTP+WebSocket server listening on the given address. 38 | func ListenWS(addr string) (Listener, error) { 39 | l, err := net.Listen("tcp", addr) 40 | if err != nil { 41 | return nil, err 42 | } 43 | wsl := &wsListener{ 44 | Listener: l, 45 | accepted: make(chan Session), 46 | } 47 | srv := &http.Server{ 48 | Addr: addr, 49 | Handler: websocket.Handler(func(ws *websocket.Conn) { 50 | ws.PayloadType = websocket.BinaryFrame 51 | sess := New(ws) 52 | defer sess.Close() 53 | wsl.accepted <- sess 54 | sess.Wait() 55 | }), 56 | } 57 | go srv.Serve(l) 58 | return wsl, nil 59 | } 60 | -------------------------------------------------------------------------------- /x/cbor/_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/progrium/qtalk-go/fn" 10 | "github.com/progrium/qtalk-go/talk" 11 | "github.com/progrium/qtalk-go/x/cbor/codec" 12 | "github.com/progrium/qtalk-go/x/cbor/mux" 13 | ) 14 | 15 | func fatal(err error) { 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | type Foo struct { 22 | S string 23 | F *Foo 24 | } 25 | 26 | type Foos []Foo 27 | 28 | func main() { 29 | l, err := net.Listen("tcp", "127.0.0.1:0") 30 | fatal(err) 31 | defer l.Close() 32 | 33 | go func() { 34 | conn, err := l.Accept() 35 | fatal(err) 36 | sess := talk.NewPeer(mux.New(conn), codec.CBORCodec{}) 37 | sess.Handle("hello", fn.HandlerFrom(func(s string, f float64, i int, b []byte) bool { 38 | log.Printf("%#v %#v %#v %#v", s, f, i, b) 39 | return true 40 | })) 41 | sess.Handle("foo", fn.HandlerFrom(func(f1 []Foo, f2 Foos) { 42 | log.Printf("%#v %#v", f1, f2) 43 | })) 44 | sess.Respond() 45 | }() 46 | 47 | conn, err := net.Dial("tcp", l.Addr().String()) 48 | fatal(err) 49 | defer conn.Close() 50 | 51 | sess := talk.NewPeer(mux.New(conn), codec.CBORCodec{}) 52 | var ret bool 53 | _, err = sess.Call(context.Background(), "hello", fn.Args{"hi", 1.23, 100, []byte("hello")}, &ret) 54 | fatal(err) 55 | fmt.Println(ret) 56 | 57 | foo := Foo{S: "Subfoo"} 58 | _, err = sess.Call(context.Background(), "foo", fn.Args{[]Foo{ 59 | Foo{S: "Foo1"}, 60 | Foo{S: "Foo2", F: &foo}, 61 | }, Foos{ 62 | Foo{S: "FooA"}, 63 | Foo{S: "FooB"}, 64 | }}, &ret) 65 | fatal(err) 66 | } 67 | -------------------------------------------------------------------------------- /rpc/proxy_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func TestProxyHandlerUnaryRPC(t *testing.T) { 11 | ctx := context.Background() 12 | 13 | backmux := NewRespondMux() 14 | backmux.Handle("simple", HandlerFunc(func(r Responder, c *Call) { 15 | r.Return("simple") 16 | })) 17 | 18 | backend, _ := newTestPair(backmux) 19 | defer backend.Close() 20 | 21 | frontmux := NewRespondMux() 22 | frontmux.Handle("", ProxyHandler(backend)) 23 | 24 | client, _ := newTestPair(frontmux) 25 | defer client.Close() 26 | 27 | var out interface{} 28 | _, err := client.Call(ctx, "simple", nil, &out) 29 | fatal(t, err) 30 | if out != "simple" { 31 | t.Fatal("unexpected return:", out) 32 | } 33 | } 34 | 35 | func TestProxyHandlerBytestream(t *testing.T) { 36 | ctx := context.Background() 37 | 38 | backmux := NewRespondMux() 39 | backmux.Handle("echo", HandlerFunc(func(r Responder, c *Call) { 40 | c.Receive(nil) 41 | ch, err := r.Continue(nil) 42 | fatal(t, err) 43 | io.Copy(ch, ch) 44 | ch.Close() 45 | })) 46 | 47 | backend, _ := newTestPair(backmux) 48 | defer backend.Close() 49 | 50 | frontmux := NewRespondMux() 51 | frontmux.Handle("", ProxyHandler(backend)) 52 | 53 | client, _ := newTestPair(frontmux) 54 | defer client.Close() 55 | 56 | resp, err := client.Call(ctx, "echo", nil, nil) 57 | fatal(t, err) 58 | _, err = io.WriteString(resp.Channel, "Hello world") 59 | fatal(t, err) 60 | fatal(t, resp.Channel.CloseWrite()) 61 | b, err := ioutil.ReadAll(resp.Channel) 62 | fatal(t, err) 63 | if string(b) != "Hello world" { 64 | t.Fatal("unexpected return data:", string(b)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /x/cbor/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= 2 | github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= 3 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 4 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 5 | github.com/progrium/qtalk-go v0.5.0 h1:4ZWIWQtDuYx/l89Jyf//xQWbomoctA/7mTkMj588gqI= 6 | github.com/progrium/qtalk-go v0.5.0/go.mod h1:vBfnZmpi9OTX/xNKQ6VOeq6uiaG3B4mhXI39K62bvn8= 7 | github.com/progrium/qtalk-go v0.5.1-0.20230305191028-54c22354090f h1:VgNsxT1XrNj15bqQt5BDO1DUAeVQ1icbjLhQPFpAe58= 8 | github.com/progrium/qtalk-go v0.5.1-0.20230305191028-54c22354090f/go.mod h1:7iMU7mMkvX9OE6OW0wSIhS8kG2T8ODnFPJnj7IB4h6Q= 9 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 10 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 11 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 12 | golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg= 13 | golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 14 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 17 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 19 | -------------------------------------------------------------------------------- /mux/frame/frame_test.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestEncodeDecode(t *testing.T) { 9 | tests := []struct { 10 | in Message 11 | id uint32 12 | ok bool 13 | }{ 14 | { 15 | in: CloseMessage{ 16 | ChannelID: 10, 17 | }, 18 | id: 10, 19 | ok: true, 20 | }, 21 | { 22 | in: DataMessage{ 23 | ChannelID: 10, 24 | Length: 5, 25 | Data: []byte("Hello"), 26 | }, 27 | id: 10, 28 | ok: true, 29 | }, 30 | { 31 | in: EOFMessage{ 32 | ChannelID: 10, 33 | }, 34 | id: 10, 35 | ok: true, 36 | }, 37 | { 38 | in: OpenMessage{ 39 | SenderID: 10, 40 | WindowSize: 1024, 41 | MaxPacketSize: 1 << 31, 42 | }, 43 | id: 0, 44 | ok: false, 45 | }, 46 | { 47 | in: OpenConfirmMessage{ 48 | ChannelID: 20, 49 | SenderID: 10, 50 | WindowSize: 1024, 51 | MaxPacketSize: 1 << 31, 52 | }, 53 | id: 20, 54 | ok: true, 55 | }, 56 | { 57 | in: OpenFailureMessage{ 58 | ChannelID: 20, 59 | }, 60 | id: 20, 61 | ok: true, 62 | }, 63 | { 64 | in: WindowAdjustMessage{ 65 | ChannelID: 20, 66 | AdditionalBytes: 1024, 67 | }, 68 | id: 20, 69 | ok: true, 70 | }, 71 | } 72 | for _, test := range tests { 73 | var buf bytes.Buffer 74 | enc := NewEncoder(&buf) 75 | if err := enc.Encode(test.in); err != nil { 76 | t.Fatal(err) 77 | } 78 | dec := NewDecoder(&buf) 79 | m, err := dec.Decode() 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | id, ok := m.Channel() 84 | if id != test.id { 85 | t.Fatal("id not equal") 86 | } 87 | if ok != test.ok { 88 | t.Fatal("ok not equal") 89 | } 90 | if m.String() == "" { 91 | t.Fatal("empty string representation") 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /mux/util_window.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // window represents the buffer available to clients 9 | // wishing to write to a channel. 10 | type window struct { 11 | *sync.Cond 12 | win uint32 // RFC 4254 5.2 says the window size can grow to 2^32-1 13 | writeWaiters int 14 | closed bool 15 | } 16 | 17 | // add adds win to the amount of window available 18 | // for consumers. 19 | func (w *window) add(win uint32) bool { 20 | // a zero sized window adjust is a noop. 21 | if win == 0 { 22 | return true 23 | } 24 | w.L.Lock() 25 | if w.win+win < win { 26 | w.L.Unlock() 27 | return false 28 | } 29 | w.win += win 30 | // It is unusual that multiple goroutines would be attempting to reserve 31 | // window space, but not guaranteed. Use broadcast to notify all waiters 32 | // that additional window is available. 33 | w.Broadcast() 34 | w.L.Unlock() 35 | return true 36 | } 37 | 38 | // close sets the window to closed, so all reservations fail 39 | // immediately. 40 | func (w *window) close() { 41 | w.L.Lock() 42 | w.closed = true 43 | w.Broadcast() 44 | w.L.Unlock() 45 | } 46 | 47 | // reserve reserves win from the available window capacity. 48 | // If no capacity remains, reserve will block. reserve may 49 | // return less than requested. 50 | func (w *window) reserve(win uint32) (uint32, error) { 51 | var err error 52 | w.L.Lock() 53 | w.writeWaiters++ 54 | w.Broadcast() 55 | for w.win == 0 && !w.closed { 56 | w.Wait() 57 | } 58 | w.writeWaiters-- 59 | if w.win < win { 60 | win = w.win 61 | } 62 | w.win -= win 63 | if w.closed { 64 | err = io.EOF 65 | } 66 | w.L.Unlock() 67 | return win, err 68 | } 69 | 70 | // waitWriterBlocked waits until some goroutine is blocked for further 71 | // writes. It is used in tests only. 72 | func (w *window) waitWriterBlocked() { 73 | w.Cond.L.Lock() 74 | for w.writeWaiters == 0 { 75 | w.Cond.Wait() 76 | } 77 | w.Cond.L.Unlock() 78 | } 79 | -------------------------------------------------------------------------------- /rpc/frame.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | 8 | "github.com/progrium/qtalk-go/codec" 9 | ) 10 | 11 | // FrameCodec is a special codec used to actually read/write other 12 | // codecs to a transport using a length prefix. 13 | type FrameCodec struct { 14 | codec.Codec 15 | } 16 | 17 | // Encoder returns a frame encoder that first encodes a value 18 | // to a buffer using the embedded codec, prepends the encoded value 19 | // byte length as a four byte big endian uint32, then writes to 20 | // the given Writer. 21 | func (c *FrameCodec) Encoder(w io.Writer) codec.Encoder { 22 | return &frameEncoder{ 23 | w: w, 24 | c: c.Codec, 25 | } 26 | } 27 | 28 | type frameEncoder struct { 29 | w io.Writer 30 | c codec.Codec 31 | } 32 | 33 | func (e *frameEncoder) Encode(v interface{}) error { 34 | var buf bytes.Buffer 35 | enc := e.c.Encoder(&buf) 36 | err := enc.Encode(v) 37 | if err != nil { 38 | return err 39 | } 40 | b := buf.Bytes() 41 | prefix := make([]byte, 4) 42 | binary.BigEndian.PutUint32(prefix, uint32(len(b))) 43 | _, err = e.w.Write(append(prefix, b...)) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | // Decoder returns a frame decoder that first reads a four byte frame 51 | // length value used to read the rest of the frame, then uses the 52 | // embedded codec to decode those bytes into a value. 53 | func (c *FrameCodec) Decoder(r io.Reader) codec.Decoder { 54 | return &frameDecoder{ 55 | r: r, 56 | c: c.Codec, 57 | } 58 | } 59 | 60 | type frameDecoder struct { 61 | r io.Reader 62 | c codec.Codec 63 | } 64 | 65 | func (d *frameDecoder) Decode(v interface{}) error { 66 | prefix := make([]byte, 4) 67 | _, err := io.ReadFull(d.r, prefix) 68 | if err != nil { 69 | return err 70 | } 71 | size := binary.BigEndian.Uint32(prefix) 72 | buf := make([]byte, size) 73 | _, err = io.ReadFull(d.r, buf) 74 | if err != nil { 75 | return err 76 | } 77 | dec := d.c.Decoder(bytes.NewBuffer(buf)) 78 | err = dec.Decode(v) 79 | if err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/progrium/qtalk-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/mitchellh/mapstructure v1.4.3 7 | github.com/rs/xid v1.3.0 8 | golang.org/x/net v0.14.0 9 | ) 10 | 11 | require ( 12 | github.com/progrium/clon-go v0.0.0-20221124010328-fe21965c77cb 13 | github.com/progrium/qtalk-go/x/cbor v0.0.0-20230306002123-cb3ad0c2cc62 14 | github.com/quic-go/quic-go v0.33.1-0.20230330052113-c9ae15295683 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/fxamacker/cbor/v2 v2.4.0 // indirect 20 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 21 | github.com/golang/mock v1.6.0 // indirect 22 | github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect 23 | github.com/google/uuid v1.3.1 // indirect 24 | github.com/onsi/ginkgo/v2 v2.9.2 // indirect 25 | github.com/pion/datachannel v1.5.5 // indirect 26 | github.com/pion/dtls/v2 v2.2.7 // indirect 27 | github.com/pion/ice/v2 v2.3.11 // indirect 28 | github.com/pion/interceptor v0.1.18 // indirect 29 | github.com/pion/logging v0.2.2 // indirect 30 | github.com/pion/mdns v0.0.8 // indirect 31 | github.com/pion/randutil v0.1.0 // indirect 32 | github.com/pion/rtcp v1.2.10 // indirect 33 | github.com/pion/rtp v1.8.1 // indirect 34 | github.com/pion/sctp v1.8.8 // indirect 35 | github.com/pion/sdp/v3 v3.0.6 // indirect 36 | github.com/pion/srtp/v2 v2.0.17 // indirect 37 | github.com/pion/stun v0.6.1 // indirect 38 | github.com/pion/transport/v2 v2.2.3 // indirect 39 | github.com/pion/turn/v2 v2.1.3 // indirect 40 | github.com/pion/webrtc/v3 v3.2.21 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/quic-go/qtls-go1-19 v0.3.2 // indirect 43 | github.com/quic-go/qtls-go1-20 v0.2.2 // indirect 44 | github.com/stretchr/testify v1.8.4 // indirect 45 | github.com/x448/float16 v0.8.4 // indirect 46 | golang.org/x/crypto v0.12.0 // indirect 47 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 48 | golang.org/x/mod v0.9.0 // indirect 49 | golang.org/x/sys v0.11.0 // indirect 50 | golang.org/x/tools v0.7.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /mux/frame/decoder.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "sync" 10 | "syscall" 11 | ) 12 | 13 | // Decoder decodes messages given an io.Reader 14 | type Decoder struct { 15 | r io.Reader 16 | sync.Mutex 17 | } 18 | 19 | func NewDecoder(r io.Reader) *Decoder { 20 | return &Decoder{r: r} 21 | } 22 | 23 | func (dec *Decoder) Decode() (Message, error) { 24 | dec.Lock() 25 | defer dec.Unlock() 26 | 27 | var msgNum [1]byte 28 | _, err := io.ReadFull(dec.r, msgNum[:]) 29 | if err != nil { 30 | var syscallErr *os.SyscallError 31 | if errors.As(err, &syscallErr) && syscallErr.Err == syscall.ECONNRESET { 32 | return nil, io.EOF 33 | } 34 | return nil, err 35 | } 36 | 37 | var msg Message 38 | msg, err = messageFrom(msgNum) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | if msgNum[0] == msgChannelData { 44 | var data struct { 45 | ChannelID uint32 46 | Length uint32 47 | } 48 | if err := binary.Read(dec.r, binary.BigEndian, &data); err != nil { 49 | return nil, err 50 | } 51 | dataMsg := msg.(*DataMessage) 52 | dataMsg.ChannelID = data.ChannelID 53 | dataMsg.Length = data.Length 54 | dataMsg.Data = make([]byte, data.Length) 55 | _, err := io.ReadFull(dec.r, dataMsg.Data) 56 | if err != nil { 57 | return nil, err 58 | } 59 | } else { 60 | if err := binary.Read(dec.r, binary.BigEndian, msg); err != nil { 61 | return nil, err 62 | } 63 | } 64 | 65 | if Debug != nil { 66 | fmt.Fprintln(Debug, ">>DEC", msg) 67 | } 68 | 69 | return msg, nil 70 | } 71 | 72 | func messageFrom(num [1]byte) (Message, error) { 73 | switch num[0] { 74 | case msgChannelOpen: 75 | return new(OpenMessage), nil 76 | case msgChannelData: 77 | return new(DataMessage), nil 78 | case msgChannelOpenConfirm: 79 | return new(OpenConfirmMessage), nil 80 | case msgChannelOpenFailure: 81 | return new(OpenFailureMessage), nil 82 | case msgChannelWindowAdjust: 83 | return new(WindowAdjustMessage), nil 84 | case msgChannelEOF: 85 | return new(EOFMessage), nil 86 | case msgChannelClose: 87 | return new(CloseMessage), nil 88 | default: 89 | return nil, fmt.Errorf("qtalk: unexpected message type %d", num[0]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /mux/util_buffer.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // buffer provides a linked list buffer for data exchange 9 | // between producer and consumer. Theoretically the buffer is 10 | // of unlimited capacity as it does no allocation of its own. 11 | type buffer struct { 12 | // protects concurrent access to head, tail and closed 13 | *sync.Cond 14 | 15 | head *element // the buffer that will be read first 16 | tail *element // the buffer that will be read last 17 | 18 | closed bool 19 | } 20 | 21 | // An element represents a single link in a linked list. 22 | type element struct { 23 | buf []byte 24 | next *element 25 | } 26 | 27 | // newBuffer returns an empty buffer that is not closed. 28 | func newBuffer() *buffer { 29 | e := new(element) 30 | b := &buffer{ 31 | Cond: sync.NewCond(new(sync.Mutex)), 32 | head: e, 33 | tail: e, 34 | } 35 | return b 36 | } 37 | 38 | // write makes buf available for Read to receive. 39 | // buf must not be modified after the call to write. 40 | func (b *buffer) write(buf []byte) { 41 | b.Cond.L.Lock() 42 | e := &element{buf: buf} 43 | b.tail.next = e 44 | b.tail = e 45 | b.Cond.Signal() 46 | b.Cond.L.Unlock() 47 | } 48 | 49 | // eof closes the buffer. Reads from the buffer once all 50 | // the data has been consumed will receive io.EOF. 51 | func (b *buffer) eof() { 52 | b.Cond.L.Lock() 53 | b.closed = true 54 | b.Cond.Signal() 55 | b.Cond.L.Unlock() 56 | } 57 | 58 | // Read reads data from the internal buffer in buf. Reads will block 59 | // if no data is available, or until the buffer is closed. 60 | func (b *buffer) Read(buf []byte) (n int, err error) { 61 | b.Cond.L.Lock() 62 | defer b.Cond.L.Unlock() 63 | 64 | for len(buf) > 0 { 65 | // if there is data in b.head, copy it 66 | if len(b.head.buf) > 0 { 67 | r := copy(buf, b.head.buf) 68 | buf, b.head.buf = buf[r:], b.head.buf[r:] 69 | n += r 70 | continue 71 | } 72 | // if there is a next buffer, make it the head 73 | if len(b.head.buf) == 0 && b.head != b.tail { 74 | b.head = b.head.next 75 | continue 76 | } 77 | 78 | // if at least one byte has been copied, return 79 | if n > 0 { 80 | break 81 | } 82 | 83 | // if nothing was read, and there is nothing outstanding 84 | // check to see if the buffer is closed. 85 | if b.closed { 86 | err = io.EOF 87 | break 88 | } 89 | // out of buffers, wait for producer 90 | b.Cond.Wait() 91 | } 92 | return 93 | } 94 | -------------------------------------------------------------------------------- /x/cbor/mux/buffer.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // buffer provides a linked list buffer for data exchange 9 | // between producer and consumer. Theoretically the buffer is 10 | // of unlimited capacity as it does no allocation of its own. 11 | type buffer struct { 12 | // protects concurrent access to head, tail and closed 13 | *sync.Cond 14 | 15 | head *element // the buffer that will be read first 16 | tail *element // the buffer that will be read last 17 | 18 | closed bool 19 | } 20 | 21 | // An element represents a single link in a linked list. 22 | type element struct { 23 | buf []byte 24 | next *element 25 | } 26 | 27 | // newBuffer returns an empty buffer that is not closed. 28 | func newBuffer() *buffer { 29 | e := new(element) 30 | b := &buffer{ 31 | Cond: sync.NewCond(new(sync.Mutex)), 32 | head: e, 33 | tail: e, 34 | } 35 | return b 36 | } 37 | 38 | // write makes buf available for Read to receive. 39 | // buf must not be modified after the call to write. 40 | func (b *buffer) write(buf []byte) { 41 | b.Cond.L.Lock() 42 | e := &element{buf: buf} 43 | b.tail.next = e 44 | b.tail = e 45 | b.Cond.Signal() 46 | b.Cond.L.Unlock() 47 | } 48 | 49 | // eof closes the buffer. Reads from the buffer once all 50 | // the data has been consumed will receive io.EOF. 51 | func (b *buffer) eof() { 52 | b.Cond.L.Lock() 53 | b.closed = true 54 | b.Cond.Signal() 55 | b.Cond.L.Unlock() 56 | } 57 | 58 | // Read reads data from the internal buffer in buf. Reads will block 59 | // if no data is available, or until the buffer is closed. 60 | func (b *buffer) Read(buf []byte) (n int, err error) { 61 | b.Cond.L.Lock() 62 | defer b.Cond.L.Unlock() 63 | 64 | for len(buf) > 0 { 65 | // if there is data in b.head, copy it 66 | if len(b.head.buf) > 0 { 67 | r := copy(buf, b.head.buf) 68 | buf, b.head.buf = buf[r:], b.head.buf[r:] 69 | n += r 70 | continue 71 | } 72 | // if there is a next buffer, make it the head 73 | if len(b.head.buf) == 0 && b.head != b.tail { 74 | b.head = b.head.next 75 | continue 76 | } 77 | 78 | // if at least one byte has been copied, return 79 | if n > 0 { 80 | break 81 | } 82 | 83 | // if nothing was read, and there is nothing outstanding 84 | // check to see if the buffer is closed. 85 | if b.closed { 86 | err = io.EOF 87 | break 88 | } 89 | // out of buffers, wait for producer 90 | b.Cond.Wait() 91 | } 92 | return 93 | } 94 | -------------------------------------------------------------------------------- /cmd/qtalk/interop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "log" 11 | "math/big" 12 | "os" 13 | 14 | "github.com/progrium/qtalk-go/cmd/qtalk/cli" 15 | "github.com/progrium/qtalk-go/codec" 16 | "github.com/progrium/qtalk-go/fn" 17 | "github.com/progrium/qtalk-go/interop" 18 | "github.com/progrium/qtalk-go/mux" 19 | "github.com/progrium/qtalk-go/rpc" 20 | cbor "github.com/progrium/qtalk-go/x/cbor/codec" 21 | qquic "github.com/progrium/qtalk-go/x/quic" 22 | "github.com/quic-go/quic-go" 23 | ) 24 | 25 | var interopCmd = &cli.Command{ 26 | Usage: "interop", 27 | Short: "run interop service", 28 | Args: cli.MaxArgs(1), 29 | Run: func(ctx context.Context, args []string) { 30 | log.SetOutput(os.Stderr) 31 | 32 | var c codec.Codec = cbor.CBORCodec{} 33 | if os.Getenv("QTALK_CODEC") == "json" { 34 | c = codec.JSONCodec{} 35 | } 36 | 37 | if len(args) == 0 { 38 | // STDIO 39 | sess, err := mux.DialStdio() 40 | fatal(err) 41 | serve(sess, c) 42 | return 43 | } 44 | 45 | // QUIC 46 | log.Printf("* Listening on %s...\n", args[0]) 47 | l, err := quic.ListenAddr(args[0], generateTLSConfig(), nil) 48 | fatal(err) 49 | defer l.Close() 50 | 51 | for { 52 | conn, err := l.Accept(context.Background()) 53 | fatal(err) 54 | go serve(qquic.New(conn), c) 55 | } 56 | }, 57 | } 58 | 59 | func serve(sess mux.Session, c codec.Codec) { 60 | srv := rpc.Server{ 61 | Handler: fn.HandlerFrom(interop.InteropService{}), 62 | Codec: c, 63 | } 64 | srv.Respond(sess, nil) 65 | } 66 | 67 | var defaultTLSConfig = tls.Config{ 68 | NextProtos: []string{"qtalk-quic"}, 69 | } 70 | 71 | func generateTLSConfig() *tls.Config { 72 | key, err := rsa.GenerateKey(rand.Reader, 1024) 73 | if err != nil { 74 | panic(err) 75 | } 76 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 77 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 78 | if err != nil { 79 | panic(err) 80 | } 81 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 82 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 83 | 84 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 85 | if err != nil { 86 | panic(err) 87 | } 88 | cfg := defaultTLSConfig.Clone() 89 | cfg.Certificates = []tls.Certificate{tlsCert} 90 | return cfg 91 | } 92 | -------------------------------------------------------------------------------- /mux/proxy_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "syscall" 10 | "testing" 11 | ) 12 | 13 | func setupProxy(t *testing.T) (func(), chan error, Session, Session) { 14 | la, err := net.Listen("tcp", "127.0.0.1:0") 15 | fatal(err, t) 16 | lb, err := net.Listen("tcp", "127.0.0.1:0") 17 | fatal(err, t) 18 | cleanup := func() { 19 | la.Close() 20 | lb.Close() 21 | } 22 | 23 | proxyErr := make(chan error, 1) 24 | go func() { 25 | a, err := la.Accept() 26 | fatal(err, t) 27 | defer a.Close() 28 | 29 | b, err := lb.Accept() 30 | fatal(err, t) 31 | defer b.Close() 32 | 33 | proxyErr <- Proxy(New(b), New(a)) 34 | }() 35 | 36 | cb, err := net.Dial("tcp", lb.Addr().String()) 37 | fatal(err, t) 38 | 39 | ca, err := net.Dial("tcp", la.Addr().String()) 40 | fatal(err, t) 41 | 42 | return cleanup, proxyErr, New(ca), New(cb) 43 | } 44 | 45 | func TestProxyDuplex(t *testing.T) { 46 | cleanup, _, sessA, sessB := setupProxy(t) 47 | defer cleanup() 48 | 49 | ctx := context.Background() 50 | chA, err := sessA.Open(ctx) 51 | fatal(err, t) 52 | 53 | chB, err := sessB.Accept() 54 | fatal(err, t) 55 | 56 | msgA := "A -> a <-> b -> B" 57 | msgB := "B -> b <-> a -> A" 58 | _, err = io.WriteString(chA, msgA) 59 | fatal(err, t) 60 | fatal(chA.CloseWrite(), t) 61 | 62 | _, err = io.WriteString(chB, msgB) 63 | fatal(err, t) 64 | fatal(chB.CloseWrite(), t) 65 | 66 | gotA, err := ioutil.ReadAll(chA) 67 | fatal(err, t) 68 | gotB, err := ioutil.ReadAll(chB) 69 | fatal(err, t) 70 | 71 | if string(gotA) != msgB { 72 | t.Fatalf("unexpected bytes read from chA: %#v", gotA) 73 | } 74 | 75 | if string(gotB) != msgA { 76 | t.Fatalf("unexpected bytes read from chB: %#v", gotB) 77 | } 78 | } 79 | 80 | func TestProxyCloseDst(t *testing.T) { 81 | cleanup, proxyErr, sessA, sessB := setupProxy(t) 82 | defer cleanup() 83 | 84 | fatal(sessB.Close(), t) 85 | 86 | ctx := context.Background() 87 | chA, err := sessA.Open(ctx) 88 | fatal(err, t) 89 | 90 | _, err = io.WriteString(chA, "hello") 91 | if err != nil && 92 | !errors.Is(err, net.ErrClosed) && 93 | !errors.Is(err, io.EOF) && 94 | !errors.Is(err, syscall.EPIPE) && 95 | !errors.Is(err, syscall.ECONNRESET) { 96 | t.Fatal("unexpected channel error:", err) 97 | } 98 | 99 | err = <-proxyErr 100 | if !errors.Is(err, net.ErrClosed) { 101 | t.Fatal("unexpected proxy error:", err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /interop/service.go: -------------------------------------------------------------------------------- 1 | package interop 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log" 8 | 9 | "github.com/progrium/qtalk-go/rpc" 10 | ) 11 | 12 | type InteropService struct{} 13 | 14 | func (s InteropService) Unary(resp rpc.Responder, call *rpc.Call) { 15 | var params any 16 | if err := call.Receive(¶ms); err != nil { 17 | log.Println(err) 18 | return 19 | } 20 | ctx := context.Background() 21 | var ret any 22 | _, err := call.Caller.Call(ctx, "Unary", params, &ret) 23 | if err != nil { 24 | log.Println(err) 25 | return 26 | } 27 | if err := resp.Return(ret); err != nil { 28 | log.Println(err) 29 | } 30 | } 31 | 32 | func (s InteropService) Stream(resp rpc.Responder, call *rpc.Call) { 33 | var params any 34 | if err := call.Receive(¶ms); err != nil { 35 | log.Println(err) 36 | return 37 | } 38 | ctx := context.Background() 39 | var ret any 40 | stream, err := call.Caller.Call(ctx, "Stream", params, &ret) 41 | if err != nil { 42 | log.Println(err) 43 | return 44 | } 45 | ch, err := resp.Continue(ret) 46 | if err != nil { 47 | log.Println(err) 48 | return 49 | } 50 | defer ch.Close() 51 | go func() { 52 | var v any 53 | var err error 54 | for { 55 | err = call.Receive(&v) 56 | if err != nil { 57 | break 58 | } 59 | err = stream.Send(v) 60 | if err != nil { 61 | break 62 | } 63 | } 64 | stream.Channel.CloseWrite() 65 | }() 66 | var v any 67 | for { 68 | err = stream.Receive(&v) 69 | if err != nil { 70 | break 71 | } 72 | err = resp.Send(v) 73 | if err != nil { 74 | break 75 | } 76 | } 77 | } 78 | 79 | func (s InteropService) Bytes(resp rpc.Responder, call *rpc.Call) { 80 | var params any 81 | if err := call.Receive(¶ms); err != nil { 82 | log.Println(err) 83 | return 84 | } 85 | ctx := context.Background() 86 | var ret any 87 | stream, err := call.Caller.Call(ctx, "Bytes", params, &ret) 88 | if err != nil { 89 | log.Println(err) 90 | return 91 | } 92 | ch, err := resp.Continue(ret) 93 | if err != nil { 94 | log.Println(err) 95 | return 96 | } 97 | defer ch.Close() 98 | go func() { 99 | io.Copy(stream.Channel, call) 100 | stream.Channel.CloseWrite() 101 | }() 102 | io.Copy(ch, stream.Channel) 103 | } 104 | 105 | func (s InteropService) Error(resp rpc.Responder, call *rpc.Call) { 106 | var text string 107 | if err := call.Receive(&text); err != nil { 108 | log.Println(err) 109 | return 110 | } 111 | if err := resp.Return(errors.New(text)); err != nil { 112 | log.Println(err) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtalk-go 2 | [![GoDoc](https://godoc.org/github.com/progrium/qtalk-go?status.svg)](https://godoc.org/github.com/progrium/qtalk-go) 3 | Test workflow 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/progrium/qtalk-go)](https://goreportcard.com/report/github.com/progrium/qtalk-go) 5 | @progriumHQ on Twitter 6 | Project Forum 7 | Sponsor Project 8 | 9 | qtalk-go is a versatile RPC and IO stream based IPC stack for Go: 10 | 11 | * client *or* server can make RPC calls to the other end 12 | * calls can be unary or streaming for multiple inputs/outputs 13 | * pluggable data codecs for flexible object stream marshaling 14 | * RPC calls designed to optionally become full-duplex byte streams 15 | * muxing layer based on subset of SSH (qmux) and soon optionally QUIC 16 | * qmux allows any `io.ReadWriteCloser` transport, including STDIO 17 | * API inspired by `net/http` with easy function/method export on top 18 | * supports passing remote callbacks over RPC 19 | 20 | The goal was to come up with the most minimal design for the most flexibility 21 | in how you want to communicate between processes. 22 | 23 | ## Getting Started 24 | ``` 25 | $ go get github.com/progrium/qtalk-go 26 | ``` 27 | The [Examples](https://github.com/progrium/qtalk-go/wiki/Examples) wiki page walks through a bunch of ways it can be used. Here are quick links: 28 | * [Simple RPC](https://github.com/progrium/qtalk-go/wiki/Examples#simple-rpc) 29 | * [Selector Routing](https://github.com/progrium/qtalk-go/wiki/Examples#selector-routing) 30 | * [Streaming Responses](https://github.com/progrium/qtalk-go/wiki/Examples#streaming-responses) 31 | * [Bytestream Proxy](https://github.com/progrium/qtalk-go/wiki/Examples#bytestream-proxy) 32 | * [Bidirectional Calling](https://github.com/progrium/qtalk-go/wiki/Examples#reverse-roles-bidirectional-calling) 33 | * [State Synchronization](https://github.com/progrium/qtalk-go/wiki/Examples#state-synchronization) 34 | 35 | ## License 36 | 37 | MIT 38 | -------------------------------------------------------------------------------- /x/quic/quic.go: -------------------------------------------------------------------------------- 1 | package quic 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "io" 7 | "strings" 8 | 9 | "github.com/progrium/qtalk-go/mux" 10 | "github.com/progrium/qtalk-go/talk" 11 | "github.com/quic-go/quic-go" 12 | ) 13 | 14 | func New(conn quic.Connection) mux.Session { 15 | return &session{conn} 16 | } 17 | 18 | var defaultTLSConfig = tls.Config{ 19 | NextProtos: []string{"qtalk-quic"}, 20 | } 21 | 22 | func Dial(addr string) (mux.Session, error) { 23 | conn, err := quic.DialAddr(addr, &defaultTLSConfig, nil) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return New(conn), nil 28 | } 29 | 30 | func init() { 31 | talk.Dialers["quic"] = Dial 32 | } 33 | 34 | type session struct { 35 | conn quic.Connection 36 | } 37 | 38 | func (s *session) Close() error { 39 | return s.conn.CloseWithError(42, "close connection") 40 | } 41 | 42 | func (s *session) Accept() (mux.Channel, error) { 43 | stream, err := s.conn.AcceptStream(context.Background()) 44 | if err != nil { 45 | if strings.Contains(err.Error(), "close connection") { 46 | return nil, io.EOF 47 | } 48 | return nil, err 49 | } 50 | header := make([]byte, 1) 51 | _, err = stream.Read(header) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return &channel{stream}, nil 56 | } 57 | 58 | func (s *session) Open(ctx context.Context) (mux.Channel, error) { 59 | // TODO Make this wait for an acknowledgement from the remote that it has 60 | // accepted the connection. It writes some data in order to notify the remote 61 | // of the new stream immediately, but my initial attempt to send an 62 | // acknowledgement from the remote side lead to deadlocks in the tests. 63 | stream, err := s.conn.OpenStreamSync(ctx) 64 | if err != nil { 65 | return nil, err 66 | } 67 | _, err = stream.Write([]byte("!")) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &channel{stream}, nil 72 | } 73 | 74 | func (s *session) Wait() error { 75 | <-s.conn.Context().Done() 76 | return s.conn.Context().Err() 77 | } 78 | 79 | type channel struct { 80 | stream quic.Stream 81 | } 82 | 83 | func (c *channel) ID() uint32 { 84 | return uint32(c.stream.StreamID()) 85 | } 86 | 87 | func (c *channel) Read(p []byte) (int, error) { 88 | return c.stream.Read(p) 89 | } 90 | 91 | func (c *channel) Write(p []byte) (int, error) { 92 | return c.stream.Write(p) 93 | } 94 | 95 | func (c *channel) Close() error { 96 | c.stream.CancelRead(42) 97 | return c.CloseWrite() 98 | } 99 | 100 | func (c *channel) CloseWrite() error { 101 | // TODO this may need a lock to avoid concurrent call with Write 102 | return c.stream.Close() 103 | } 104 | -------------------------------------------------------------------------------- /rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net" 8 | 9 | "github.com/progrium/qtalk-go/codec" 10 | "github.com/progrium/qtalk-go/mux" 11 | ) 12 | 13 | // Server wraps a Handler and codec to respond to RPC calls. 14 | type Server struct { 15 | Handler Handler 16 | Codec codec.Codec 17 | } 18 | 19 | // ServeMux will Accept sessions until the Listener is closed, and will Respond to accepted sessions in their own goroutine. 20 | func (s *Server) ServeMux(l mux.Listener) error { 21 | for { 22 | sess, err := l.Accept() 23 | if err != nil { 24 | return err 25 | } 26 | go s.Respond(sess, nil) 27 | } 28 | } 29 | 30 | // Serve will Accept sessions until the Listener is closed, and will Respond to accepted sessions in their own goroutine. 31 | func (s *Server) Serve(l net.Listener) error { 32 | return s.ServeMux(mux.ListenerFrom(l)) 33 | } 34 | 35 | // Respond will Accept channels until the Session is closed and respond with the server handler in its own goroutine. 36 | // If Handler was not set, an empty RespondMux is used. If the handler does not initiate a response, a nil value is 37 | // returned. If the handler does not call Continue, the channel will be closed. Respond will panic if Codec is nil. 38 | // 39 | // If the context is not nil, it will be added to Calls. Otherwise the Call Context will be set to a context.Background(). 40 | func (s *Server) Respond(sess mux.Session, ctx context.Context) { 41 | defer sess.Close() 42 | 43 | if s.Codec == nil { 44 | panic("rpc.Respond: nil codec") 45 | } 46 | 47 | hn := s.Handler 48 | if hn == nil { 49 | hn = NewRespondMux() 50 | } 51 | 52 | for { 53 | ch, err := sess.Accept() 54 | if err != nil { 55 | if err == io.EOF { 56 | return 57 | } 58 | panic(err) 59 | } 60 | go s.respond(hn, sess, ch, ctx) 61 | } 62 | } 63 | 64 | func (s *Server) respond(hn Handler, sess mux.Session, ch mux.Channel, ctx context.Context) { 65 | framer := &FrameCodec{Codec: s.Codec} 66 | dec := framer.Decoder(ch) 67 | 68 | var call Call 69 | err := dec.Decode(&call) 70 | if err != nil { 71 | log.Println("rpc.Respond:", err) 72 | return 73 | } 74 | 75 | call.Selector = cleanSelector(call.Selector) 76 | call.Decoder = dec 77 | call.Caller = &Client{ 78 | Session: sess, 79 | codec: s.Codec, 80 | } 81 | if ctx == nil { 82 | call.Context = context.Background() 83 | } else { 84 | call.Context = ctx 85 | } 86 | call.Channel = ch 87 | 88 | header := &ResponseHeader{} 89 | resp := &responder{ 90 | ch: ch, 91 | c: framer, 92 | header: header, 93 | } 94 | 95 | hn.RespondRPC(resp, &call) 96 | if !resp.responded { 97 | resp.Return() 98 | } 99 | if !resp.header.Continue { 100 | ch.Close() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /exp/ptr_test.go: -------------------------------------------------------------------------------- 1 | package exp_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | fn "github.com/progrium/qtalk-go/exp" 9 | "github.com/progrium/qtalk-go/rpc" 10 | ) 11 | 12 | type mockCaller struct { 13 | selector string 14 | params, reply interface{} 15 | } 16 | 17 | func (mc *mockCaller) Call(ctx context.Context, selector string, params any, reply ...any) (*rpc.Response, error) { 18 | mc.selector = selector 19 | mc.params = params 20 | mc.reply = reply 21 | return nil, nil 22 | } 23 | 24 | type mockData struct { 25 | Inner struct { 26 | Fn *fn.Ptr 27 | } 28 | NilFn *fn.Ptr 29 | Fn *fn.Ptr 30 | } 31 | 32 | func TestPtrCall(t *testing.T) { 33 | cb := fn.Callback(func() {}) 34 | data := mockData{Fn: cb} 35 | caller := &mockCaller{} 36 | fn.SetCallers(&data, caller) 37 | var ret interface{} 38 | cb.Call(context.Background(), []int{1, 2, 3}, &ret) 39 | if len(caller.params.([]int)) != 3 { 40 | t.Fatal("unexpected params:", caller.params) 41 | } 42 | if cb.Ptr != caller.selector { 43 | t.Fatal("unexpected selector:", caller.selector) 44 | } 45 | } 46 | 47 | func TestPtrsFromMap(t *testing.T) { 48 | data := mockData{Fn: fn.Callback(func() {})} 49 | 50 | b, _ := json.Marshal(data) 51 | var m map[string]interface{} 52 | json.Unmarshal(b, &m) 53 | 54 | caller := &mockCaller{} 55 | fn.SetCallers(&m, caller) 56 | 57 | if m["Fn"].(map[string]interface{})["Caller"] != caller { 58 | t.Fatal("caller not set") 59 | } 60 | 61 | } 62 | 63 | func TestCallbackUtils(t *testing.T) { 64 | data := mockData{ 65 | Fn: fn.Callback(func() { 66 | //outerCalled = true 67 | }), 68 | } 69 | data.Inner.Fn = fn.Callback(func() { 70 | //innerCalled = true 71 | }) 72 | 73 | caller := &mockCaller{} 74 | fn.SetCallers(&data, caller) 75 | 76 | t.Run("SetCallers", func(t *testing.T) { 77 | if data.Fn.Caller != caller { 78 | t.Fatal("outer caller not set") 79 | } 80 | 81 | if data.Inner.Fn.Caller != caller { 82 | t.Fatal("inner caller not set") 83 | } 84 | }) 85 | 86 | mux := rpc.NewRespondMux() 87 | fn.RegisterPtrs(mux, data) 88 | 89 | t.Run("RegisterPtrs", func(t *testing.T) { 90 | h, _ := mux.Match(data.Fn.Ptr) 91 | if h == nil { 92 | t.Fatal("outer handler not found") 93 | } 94 | 95 | h, _ = mux.Match(data.Inner.Fn.Ptr) 96 | if h == nil { 97 | t.Fatal("inner handler not found") 98 | } 99 | }) 100 | 101 | fn.UnregisterPtrs(mux, data) 102 | 103 | t.Run("UnregisterPtrs", func(t *testing.T) { 104 | h, _ := mux.Match(data.Fn.Ptr) 105 | if h != nil { 106 | t.Fatal("outer handler still found") 107 | } 108 | 109 | h, _ = mux.Match(data.Inner.Fn.Ptr) 110 | if h != nil { 111 | t.Fatal("inner handler still found") 112 | } 113 | }) 114 | 115 | } 116 | -------------------------------------------------------------------------------- /cmd/qtalk/bench.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "time" 14 | 15 | "github.com/progrium/qtalk-go/cmd/qtalk/cli" 16 | "github.com/progrium/qtalk-go/codec" 17 | "github.com/progrium/qtalk-go/fn" 18 | "github.com/progrium/qtalk-go/interop" 19 | "github.com/progrium/qtalk-go/mux" 20 | "github.com/progrium/qtalk-go/rpc" 21 | cbor "github.com/progrium/qtalk-go/x/cbor/codec" 22 | qquic "github.com/progrium/qtalk-go/x/quic" 23 | "github.com/quic-go/quic-go" 24 | ) 25 | 26 | var benchCmd = &cli.Command{ 27 | Usage: "bench", 28 | Short: "interop benchmark", 29 | Run: func(ctx context.Context, args []string) { 30 | log.SetOutput(os.Stderr) 31 | 32 | var c codec.Codec = cbor.CBORCodec{} 33 | if os.Getenv("QTALK_CODEC") == "json" { 34 | log.Println("* Using JSON codec") 35 | c = codec.JSONCodec{} 36 | } 37 | 38 | var cmd *exec.Cmd 39 | var sess mux.Session 40 | 41 | if len(args) == 0 { 42 | // self check 43 | path, err := os.Executable() 44 | fatal(err) 45 | cmd = exec.Command(path, "interop") 46 | } else if !strings.HasPrefix(args[0], "udp://") { 47 | // check against subprocess 48 | path, err := exec.LookPath("sh") 49 | fatal(err) 50 | cmd = exec.Command(path, "-c", args[0]) 51 | } 52 | 53 | if cmd != nil { 54 | cmd.Stderr = os.Stderr 55 | wc, err := cmd.StdinPipe() 56 | if err != nil { 57 | fatal(err) 58 | } 59 | rc, err := cmd.StdoutPipe() 60 | if err != nil { 61 | fatal(err) 62 | } 63 | sess, err = mux.DialIO(wc, rc) 64 | if err != nil { 65 | fatal(err) 66 | } 67 | if err := cmd.Start(); err != nil { 68 | fatal(err) 69 | } 70 | defer func() { 71 | cmd.Process.Signal(os.Interrupt) 72 | cmd.Wait() 73 | }() 74 | } else { 75 | // check against remote quic endpoint 76 | cfg := defaultTLSConfig.Clone() 77 | cfg.InsecureSkipVerify = true 78 | conn, err := quic.DialAddr(strings.TrimPrefix(args[0], "udp://"), cfg, nil) 79 | fatal(err) 80 | sess = qquic.New(conn) 81 | } 82 | 83 | defer sess.Close() 84 | 85 | srv := rpc.Server{ 86 | Handler: fn.HandlerFrom(interop.CallbackService{}), 87 | Codec: c, 88 | } 89 | go srv.Respond(sess, nil) 90 | 91 | caller := rpc.NewClient(sess, c) 92 | //var ret any 93 | //var err error 94 | 95 | // Bytes check 96 | // 1mb 97 | mb := 1 << 20 98 | for _, v := range []int{mb * 256, mb * 512, mb * 1024} { 99 | data := make([]byte, v) 100 | rand.Read(data) 101 | start := time.Now() 102 | resp, err := caller.Call(ctx, "Bytes", nil, nil) 103 | fatal(err) 104 | var buf bytes.Buffer 105 | go func() { 106 | io.Copy(resp.Channel, bytes.NewBuffer(data)) 107 | resp.Channel.CloseWrite() 108 | }() 109 | io.Copy(&buf, resp.Channel) 110 | if buf.Len() != len(data) { 111 | log.Fatal("byte stream buffer does not match") 112 | } 113 | diff := time.Now().Sub(start) 114 | fmt.Println("Bytes:", buf.Len()/mb, "MB", "RTT:", diff, "Thru:", int(float64(buf.Len())/diff.Seconds()/(1024*1024)), "MB/s") 115 | } 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /mux/transport_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "io/ioutil" 8 | "path" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func testExchange(t *testing.T, sess Session) { 14 | var err error 15 | var ch Channel 16 | t.Run("session accept", func(t *testing.T) { 17 | ch, err = sess.Accept() 18 | fatal(err, t) 19 | }) 20 | 21 | t.Run("channel write", func(t *testing.T) { 22 | _, err = ch.Write([]byte("Hello world")) 23 | fatal(err, t) 24 | err = ch.Close() 25 | fatal(err, t) 26 | }) 27 | 28 | t.Run("session open", func(t *testing.T) { 29 | ch, err = sess.Open(context.Background()) 30 | fatal(err, t) 31 | }) 32 | 33 | var b []byte 34 | t.Run("channel read", func(t *testing.T) { 35 | b, err = ioutil.ReadAll(ch) 36 | fatal(err, t) 37 | err = ch.Close() 38 | fatal(err, t) 39 | }) 40 | 41 | if !bytes.Equal(b, []byte("Hello world")) { 42 | t.Fatalf("unexpected bytes: %s", b) 43 | } 44 | 45 | t.Run("session close", func(t *testing.T) { 46 | err = sess.Close() 47 | fatal(err, t) 48 | }) 49 | } 50 | 51 | func startListener(t *testing.T, l Listener) { 52 | t.Helper() 53 | 54 | t.Cleanup(func() { 55 | fatal(l.Close(), t) 56 | }) 57 | 58 | go func() { 59 | sess, err := l.Accept() 60 | fatal(err, t) 61 | t.Cleanup(func() { 62 | // Synchronizes cleanup, waiting for the client to disconnect before 63 | // closing the stream. This prevents errors in the Pipe-based test with 64 | // closing one end of the pipe before the other has read the data. 65 | // Registering as a test cleanup function also avoids a race condition 66 | // with the test exiting before closing the session. 67 | if err := sess.Wait(); err != io.EOF { 68 | // windows test environments make wait return funky errors 69 | if !strings.Contains(err.Error(), "wsarecv") { 70 | t.Errorf("Wait returned unexpected error: %v", err) 71 | } 72 | } 73 | err = sess.Close() 74 | fatal(err, t) 75 | }) 76 | 77 | ch, err := sess.Open(context.Background()) 78 | fatal(err, t) 79 | b, err := ioutil.ReadAll(ch) 80 | fatal(err, t) 81 | ch.Close() 82 | 83 | ch, err = sess.Accept() 84 | _, err = ch.Write(b) 85 | fatal(err, t) 86 | err = ch.CloseWrite() 87 | fatal(err, t) 88 | }() 89 | } 90 | 91 | func TestTCP(t *testing.T) { 92 | l, err := ListenTCP("127.0.0.1:0") 93 | fatal(err, t) 94 | startListener(t, l) 95 | 96 | sess, err := DialTCP(l.Addr().String()) 97 | fatal(err, t) 98 | testExchange(t, sess) 99 | } 100 | 101 | func TestUnix(t *testing.T) { 102 | tmp := t.TempDir() 103 | sockPath := path.Join(tmp, "qmux.sock") 104 | l, err := ListenUnix(sockPath) 105 | fatal(err, t) 106 | startListener(t, l) 107 | 108 | sess, err := DialUnix(sockPath) 109 | fatal(err, t) 110 | testExchange(t, sess) 111 | } 112 | 113 | func TestIO(t *testing.T) { 114 | pr1, pw1 := io.Pipe() 115 | pr2, pw2 := io.Pipe() 116 | 117 | l, err := ListenIO(pw1, pr2) 118 | fatal(err, t) 119 | startListener(t, l) 120 | 121 | sess, err := DialIO(pw2, pr1) 122 | fatal(err, t) 123 | testExchange(t, sess) 124 | } 125 | 126 | func TestWS(t *testing.T) { 127 | l, err := ListenWS("127.0.0.1:0") 128 | fatal(err, t) 129 | startListener(t, l) 130 | 131 | sess, err := DialWS(l.Addr().String()) 132 | fatal(err, t) 133 | testExchange(t, sess) 134 | } 135 | -------------------------------------------------------------------------------- /x/cbor/mux/channel.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // channel is an implementation of the Channel interface that works 10 | // with the session class. 11 | type channel struct { 12 | localId, remoteId uint32 13 | session *session 14 | 15 | // Pending internal channel frames. 16 | frames chan Frame 17 | 18 | sentEOF bool 19 | 20 | // thread-safe data 21 | pending *buffer 22 | 23 | // writeMu serializes calls to session.conn.Write() and 24 | // protects sentClose. 25 | writeMu sync.Mutex 26 | sentClose bool 27 | } 28 | 29 | // ID returns the unique identifier of this channel 30 | // within the session 31 | func (ch *channel) ID() uint32 { 32 | return ch.localId 33 | } 34 | 35 | // CloseWrite signals the end of sending data. 36 | // The other side may still send data 37 | func (ch *channel) CloseWrite() error { 38 | ch.sentEOF = true 39 | return ch.send(Frame{ 40 | Type: channelEOF, 41 | ChannelID: ch.remoteId, 42 | }) 43 | } 44 | 45 | // Close signals end of channel use. No data may be sent after this 46 | // call. 47 | func (ch *channel) Close() error { 48 | return ch.send(Frame{ 49 | Type: channelClose, 50 | ChannelID: ch.remoteId, 51 | }) 52 | } 53 | 54 | // Write writes len(data) bytes to the channel. 55 | func (ch *channel) Write(data []byte) (n int, err error) { 56 | if ch.sentEOF { 57 | return 0, io.EOF 58 | } 59 | 60 | err = ch.session.enc.Encode(Frame{ 61 | Type: channelData, 62 | ChannelID: ch.remoteId, 63 | Data: data, 64 | }) 65 | 66 | return n, err 67 | } 68 | 69 | // Read reads up to len(data) bytes from the channel. 70 | func (c *channel) Read(data []byte) (n int, err error) { 71 | return c.pending.Read(data) 72 | } 73 | 74 | // sends writes a message frame. If the message is a channel close, it updates 75 | // sentClose. This method takes the lock c.writeMu. 76 | func (ch *channel) send(f Frame) error { 77 | ch.writeMu.Lock() 78 | defer ch.writeMu.Unlock() 79 | 80 | if ch.sentClose { 81 | return io.EOF 82 | } 83 | 84 | if f.Type == channelClose { 85 | ch.sentClose = true 86 | } 87 | 88 | return ch.session.enc.Encode(f) 89 | } 90 | 91 | func (c *channel) close() { 92 | c.pending.eof() 93 | close(c.frames) 94 | c.writeMu.Lock() 95 | // This is not necessary for a normal channel teardown, but if 96 | // there was another error, it is. 97 | c.sentClose = true 98 | c.writeMu.Unlock() 99 | } 100 | 101 | func (ch *channel) handle(f Frame) error { 102 | switch f.Type { 103 | case channelData: 104 | ch.pending.write(f.Data) 105 | return nil 106 | 107 | case channelClose: 108 | ch.send(Frame{ 109 | Type: channelClose, 110 | ChannelID: ch.remoteId, 111 | }) 112 | ch.session.chanMu.Lock() 113 | delete(ch.session.chans, ch.localId) 114 | ch.session.chanMu.Unlock() 115 | ch.close() 116 | return nil 117 | 118 | case channelEOF: 119 | ch.pending.eof() 120 | return nil 121 | 122 | case channelOpenConfirm: 123 | ch.remoteId = f.SenderID 124 | ch.frames <- f 125 | return nil 126 | 127 | case channelOpenFailure: 128 | ch.session.chanMu.Lock() 129 | delete(ch.session.chans, f.ChannelID) 130 | ch.session.chanMu.Unlock() 131 | ch.frames <- f 132 | return nil 133 | 134 | default: 135 | return fmt.Errorf("cmux: invalid channel frame %v", f) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /fn/call.go: -------------------------------------------------------------------------------- 1 | package fn 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/mitchellh/mapstructure" 8 | ) 9 | 10 | var errorInterface = reflect.TypeOf((*error)(nil)).Elem() 11 | 12 | // Call wraps invoking a function via reflection, converting the arguments with 13 | // ArgsTo and the returns with ParseReturn. fn argument can be a function 14 | // or a reflect.Value for a function. 15 | func Call(fn any, args []any) (_ []any, err error) { 16 | defer func() { 17 | if p := recover(); p != nil { 18 | err = fmt.Errorf("panic: %s [%s]", p, identifyPanic()) 19 | } 20 | }() 21 | fnval := reflect.ValueOf(fn) 22 | if rv, ok := fn.(reflect.Value); ok { 23 | fnval = rv 24 | } 25 | fnParams, err := ArgsTo(fnval.Type(), args) 26 | if err != nil { 27 | return nil, err 28 | } 29 | fnReturn := fnval.Call(fnParams) 30 | return ParseReturn(fnReturn) 31 | } 32 | 33 | // ArgsTo converts the arguments into `reflect.Value`s suitable to pass as 34 | // parameters to a function with the given type via reflection. 35 | func ArgsTo(fntyp reflect.Type, args []any) ([]reflect.Value, error) { 36 | if len(args) != fntyp.NumIn() { 37 | return nil, fmt.Errorf("fn: expected %d params, got %d", fntyp.NumIn(), len(args)) 38 | } 39 | fnParams := make([]reflect.Value, len(args)) 40 | for idx, param := range args { 41 | switch fntyp.In(idx).Kind() { 42 | case reflect.Struct: 43 | // decode to struct type using mapstructure 44 | arg := reflect.New(fntyp.In(idx)) 45 | if err := mapstructure.Decode(param, arg.Interface()); err != nil { 46 | return nil, fmt.Errorf("fn: mapstructure: %s", err.Error()) 47 | } 48 | fnParams[idx] = ensureType(arg.Elem(), fntyp.In(idx)) 49 | case reflect.Slice: 50 | rv := reflect.ValueOf(param) 51 | // decode slice of structs to struct type using mapstructure 52 | if fntyp.In(idx).Elem().Kind() == reflect.Struct { 53 | nv := reflect.MakeSlice(fntyp.In(idx), rv.Len(), rv.Len()) 54 | for i := 0; i < rv.Len(); i++ { 55 | ref := reflect.New(nv.Index(i).Type()) 56 | if err := mapstructure.Decode(rv.Index(i).Interface(), ref.Interface()); err != nil { 57 | return nil, fmt.Errorf("fn: mapstructure: %s", err.Error()) 58 | } 59 | nv.Index(i).Set(reflect.Indirect(ref)) 60 | } 61 | rv = nv 62 | } 63 | fnParams[idx] = rv 64 | default: 65 | // if int is expected but got float64 assume json-like encoding and cast float to int 66 | if fntyp.In(idx).Kind() == reflect.Int && reflect.TypeOf(param).Kind() == reflect.Float64 { 67 | param = int(param.(float64)) 68 | } 69 | fnParams[idx] = ensureType(reflect.ValueOf(param), fntyp.In(idx)) 70 | } 71 | } 72 | return fnParams, nil 73 | } 74 | 75 | // ParseReturn splits the results of reflect.Call() into the values, and 76 | // possibly an error. 77 | // If the last value is a non-nil error, this will return `nil, err`. 78 | // If the last value is a nil error it will be removed from the value list. 79 | // Any remaining values will be converted and returned as `any` typed values. 80 | func ParseReturn(ret []reflect.Value) ([]any, error) { 81 | if len(ret) == 0 { 82 | return nil, nil 83 | } 84 | last := ret[len(ret)-1] 85 | if last.Type().Implements(errorInterface) { 86 | if !last.IsNil() { 87 | return nil, last.Interface().(error) 88 | } 89 | ret = ret[:len(ret)-1] 90 | } 91 | out := make([]any, len(ret)) 92 | for i, r := range ret { 93 | out[i] = r.Interface() 94 | } 95 | return out, nil 96 | } 97 | -------------------------------------------------------------------------------- /fn/call_test.go: -------------------------------------------------------------------------------- 1 | package fn 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func fatal(err error, t *testing.T) { 11 | t.Helper() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | } 16 | 17 | func callParseReturn(fn interface{}, args []reflect.Value) ([]any, error) { 18 | ret := reflect.ValueOf(fn).Call(args) 19 | return ParseReturn(ret) 20 | } 21 | 22 | func values(args ...any) []reflect.Value { 23 | r := make([]reflect.Value, len(args)) 24 | for i, a := range args { 25 | r[i] = reflect.ValueOf(a) 26 | } 27 | return r 28 | } 29 | 30 | func equal(expected, actual []any) bool { 31 | if len(expected) == 0 { 32 | return len(actual) == 0 33 | } 34 | return reflect.DeepEqual(expected, actual) 35 | } 36 | 37 | func TestCallCatchPanic(t *testing.T) { 38 | ret, err := Call(func() { panic("catch me!") }, nil) 39 | if ret != nil { 40 | t.Errorf("expected nil return, got: %v", ret) 41 | } 42 | if err == nil || !strings.Contains(err.Error(), "catch me!") { 43 | t.Errorf("expected to panic info in an error, got: %v", err) 44 | } 45 | } 46 | 47 | func TestCallArgStructSlice(t *testing.T) { 48 | type arg struct{ V int } 49 | 50 | acutal, err := Call(func(vs []arg) int { 51 | sum := 0 52 | for _, v := range vs { 53 | sum += v.V 54 | } 55 | return sum 56 | }, []any{[]arg{{1}, {2}, {3}}}) 57 | if err != nil { 58 | t.Fatalf("unexpected error: %v", err) 59 | } 60 | expected := []any{int(6)} 61 | if !reflect.DeepEqual(expected, acutal) { 62 | t.Errorf("expected %#v, got %#v", expected, acutal) 63 | } 64 | } 65 | 66 | func TestParseReturn(t *testing.T) { 67 | tests := []struct { 68 | name string 69 | fn interface{} 70 | args []reflect.Value 71 | expected []any 72 | expectedErr bool 73 | }{ 74 | {"no return values", func() {}, nil, nil, false}, 75 | { 76 | "single value return", func(i int) int { return i * 2 }, values(int(21)), 77 | []any{int(42)}, false, 78 | }, 79 | { 80 | "multiple value return", func(i int) (int, float64) { 81 | return i * 2, float64(i) / 2 82 | }, values(int(21)), 83 | []any{int(42), float64(10.5)}, false, 84 | }, 85 | 86 | {"return nil error", func() error { return nil }, nil, nil, false}, 87 | {"return non-nil error", func() error { return fmt.Errorf("an error") }, nil, nil, true}, 88 | 89 | { 90 | "single value with nil error", func() (int, error) { return 42, nil }, nil, 91 | []any{int(42)}, false, 92 | }, 93 | { 94 | "single value with non-nil error", func() (int, error) { return 42, fmt.Errorf("an error") }, nil, 95 | nil, true, 96 | }, 97 | 98 | { 99 | "multiple value with nil error", func() (int, float64, error) { return 42, 0.5, nil }, nil, 100 | []any{int(42), float64(0.5)}, false, 101 | }, 102 | { 103 | "multiple value with non-nil error", func() (int, float64, error) { return 42, 0.5, fmt.Errorf("an error") }, nil, 104 | nil, true, 105 | }, 106 | 107 | { 108 | "return error as value", func() any { return fmt.Errorf("an error") }, nil, 109 | []any{fmt.Errorf("an error")}, false, 110 | }, 111 | } 112 | for _, td := range tests { 113 | t.Run(td.name, func(t *testing.T) { 114 | actual, err := callParseReturn(td.fn, td.args) 115 | if !td.expectedErr { 116 | fatal(err, t) 117 | } else if err == nil { 118 | t.Fatalf("expected an error") 119 | } 120 | if !equal(td.expected, actual) { 121 | t.Errorf("expected: %v\ngot: %v", td.expected, actual) 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /cmd/qtalk/cli/framework.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "reflect" 11 | "strconv" 12 | ) 13 | 14 | // Initializer is a hook to allow units to customize the root Command. 15 | type Initializer interface { 16 | InitializeCLI(root *Command) 17 | } 18 | 19 | // Runner is a unit that takes over the program entrypoint. 20 | type Runner interface { 21 | Run(ctx context.Context) error 22 | } 23 | 24 | // Framework manages a root command, allowing Initializers 25 | // to modify it, which by default runs a DefaultRunner. 26 | type Framework struct { 27 | DefaultRunner Runner 28 | Initializers []Initializer 29 | Root *Command 30 | } 31 | 32 | // Initialize sets up a Root command that simply runs the 33 | // DefaultRunner, and also runs any Initializers. 34 | func (f *Framework) Initialize() { 35 | f.Root = &Command{ 36 | Run: func(ctx context.Context, args []string) { 37 | if f.DefaultRunner == f { 38 | panic("only runner is cli.Framework") 39 | } 40 | if err := f.DefaultRunner.Run(ctx); err != nil { 41 | log.Fatal(err) 42 | } 43 | }, 44 | } 45 | for _, i := range f.Initializers { 46 | i.InitializeCLI(f.Root) 47 | } 48 | } 49 | 50 | // Run executes the root command with os.Args and STDIO. 51 | func (f *Framework) Run(ctx context.Context) error { 52 | return Execute(ContextWithIO(ctx, os.Stdin, os.Stdout, os.Stderr), f.Root, os.Args[1:]) 53 | } 54 | 55 | // Execute takes a root Command plus arguments, finds the Command to run, 56 | // parses flags, checks for expected arguments, and runs the Command. 57 | // It also adds a version flag if the root Command has Version set. 58 | func Execute(ctx context.Context, root *Command, args []string) error { 59 | var ( 60 | stdout io.Writer = os.Stdout 61 | stderr io.Writer = os.Stderr 62 | ) 63 | if IOFrom(ctx) != nil { 64 | stdout = IOFrom(ctx) 65 | stderr = IOFrom(ctx).Err() 66 | } 67 | 68 | var showVersion bool 69 | if root.Version != "" { 70 | root.Flags().BoolVar(&showVersion, "v", false, "show version") 71 | } 72 | 73 | cmd, n := root.Find(args) 74 | f := cmd.Flags() 75 | if f != nil { 76 | if err := f.Parse(args[n:]); err != nil { 77 | if err == flag.ErrHelp { 78 | return (&CommandHelp{cmd}).WriteHelp(stderr) 79 | } 80 | return err 81 | } 82 | } 83 | 84 | if showVersion { 85 | fmt.Fprintln(stdout, root.Version) 86 | return nil 87 | } 88 | 89 | if cmd.Args != nil { 90 | if err := cmd.Args(cmd, f.Args()); err != nil { 91 | return err 92 | } 93 | } 94 | 95 | if cmd.Run == nil { 96 | (&CommandHelp{cmd}).WriteHelp(stderr) 97 | return nil 98 | } 99 | 100 | cmd.Run(ctx, f.Args()) 101 | return nil 102 | } 103 | 104 | // Export wraps a function as a command. 105 | func Export(fn interface{}, use string) *Command { 106 | rv := reflect.ValueOf(fn) 107 | t := rv.Type() 108 | if t.Kind() != reflect.Func { 109 | panic("can only export funcs") 110 | } 111 | return &Command{ 112 | Usage: use, 113 | Args: ExactArgs(t.NumIn()), 114 | Run: func(ctx context.Context, args []string) { 115 | var in []reflect.Value 116 | for n := 0; n < t.NumIn(); n++ { 117 | switch t.In(n).Kind() { 118 | case reflect.String: 119 | in = append(in, reflect.ValueOf(args[n])) 120 | case reflect.Int: 121 | arg, err := strconv.Atoi(args[n]) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | in = append(in, reflect.ValueOf(arg)) 126 | default: 127 | panic("argument kind not supported: " + t.In(n).Kind().String()) 128 | } 129 | } 130 | rv.Call(in) 131 | }, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /x/webrtc/_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/base64" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/pion/webrtc/v3" 16 | "github.com/progrium/qtalk-go/mux" 17 | qtalkwebrpc "github.com/progrium/qtalk-go/x/webrtc" 18 | ) 19 | 20 | // Usage: 21 | // Start one terminal with an offer, and another with "-a" to answer: 22 | // $ go run main.go 23 | // $ go run main.go -a 24 | // 25 | // Copy the SDP from the offer output to the answer. 26 | // Copy the answer SDP to the offer. 27 | 28 | var isAnswer = flag.Bool("a", false, "answer mode") 29 | 30 | func fatal(err error, args ...any) { 31 | if err != nil { 32 | log.Fatal(append(args, err)...) 33 | } 34 | } 35 | 36 | func main() { 37 | flag.Parse() 38 | cfg := webrtc.Configuration{ 39 | ICEServers: []webrtc.ICEServer{ 40 | { 41 | URLs: []string{"stun:stun.l.google.com:19302"}, 42 | }, 43 | }, 44 | } 45 | var peer *webrtc.PeerConnection 46 | var err error 47 | if *isAnswer { 48 | log.Println("paste SDP from offer:") 49 | peer, err = qtalkwebrpc.Answer(cfg, getSDP()) 50 | } else { 51 | log.Println("creating offer...") 52 | peer, err = qtalkwebrpc.Offer(cfg) 53 | } 54 | fatal(err) 55 | 56 | // For simple case, wait to gather ICE candidates before printing the SDP 57 | log.Println("gathering ICE candidates...") 58 | 59 | // peer.OnICECandidate(func(c *webrtc.ICECandidate) { 60 | // go func() { 61 | // log.Println("got ICE candidate, latest SDP:") 62 | // printDesc(peer) 63 | // }() 64 | // }) 65 | 66 | <-webrtc.GatheringCompletePromise(peer) 67 | log.Println("gathering complete, SDP is:") 68 | printDesc(peer) 69 | 70 | if !*isAnswer { 71 | log.Println("paste SDP from answer:") 72 | fatal(peer.SetRemoteDescription(getSDP())) 73 | } 74 | 75 | sess := qtalkwebrpc.New(peer) 76 | defer sess.Close() 77 | log.Println("got session") 78 | var ch mux.Channel 79 | if *isAnswer { 80 | ch, err = sess.Open(context.Background()) 81 | } else { 82 | ch, err = sess.Accept() 83 | } 84 | fatal(err) 85 | defer ch.Close() 86 | log.Println("got channel") 87 | 88 | tick := time.Tick(1 * time.Second) 89 | ctx, cancel := context.WithCancel(context.Background()) 90 | defer cancel() 91 | 92 | go func() { 93 | for i := 0; i < 10; i++ { 94 | select { 95 | case <-tick: 96 | log.Println("sending: ping") 97 | _, err := ch.Write([]byte("ping\n")) 98 | fatal(err, "send ping") 99 | case <-ctx.Done(): 100 | return 101 | } 102 | } 103 | fatal(ch.CloseWrite(), "close write") 104 | }() 105 | 106 | r := bufio.NewReader(ch) 107 | for { 108 | line, err := r.ReadString('\n') 109 | fatal(err, "read line") 110 | log.Println("read: ", line) 111 | } 112 | } 113 | 114 | func printDesc(peer *webrtc.PeerConnection) { 115 | desc := peer.LocalDescription() 116 | b, err := json.Marshal(desc) 117 | fatal(err) 118 | s := base64.URLEncoding.EncodeToString(b) 119 | for len(s) > 80 { 120 | fmt.Println(s[:80]) 121 | s = s[80:] 122 | } 123 | fmt.Println(s) 124 | fmt.Println() 125 | } 126 | 127 | func getSDP() webrtc.SessionDescription { 128 | r := bufio.NewReader(os.Stdin) 129 | var lines []string 130 | for { 131 | b, err := r.ReadString('\n') 132 | fatal(err) 133 | if b == "\n" { 134 | break 135 | } 136 | lines = append(lines, b) 137 | } 138 | s, err := base64.URLEncoding.DecodeString(strings.Join(lines, "")) 139 | fatal(err) 140 | log.Println("json:", string(s)) 141 | var desc webrtc.SessionDescription 142 | fatal(json.Unmarshal(s, &desc)) 143 | return desc 144 | } 145 | -------------------------------------------------------------------------------- /cmd/qtalk/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | // Tests TODO: 11 | // top level help 12 | // command help 13 | // hidden commands 14 | // command aliases 15 | // help, examples 16 | 17 | func TestSimpleCommand(t *testing.T) { 18 | cmd := &Command{ 19 | Usage: "simple", 20 | Run: func(ctx context.Context, args []string) { 21 | fmt.Fprint(IOFrom(ctx), "Hello world") 22 | }, 23 | } 24 | var buf bytes.Buffer 25 | ctx := ContextWithIO(context.Background(), nil, &buf, nil) 26 | if err := Execute(ctx, cmd, []string{}); err != nil { 27 | t.Fatal(err) 28 | } 29 | if buf.String() != "Hello world" { 30 | t.Fatal("unexpected output") 31 | } 32 | } 33 | 34 | func TestPositionalArgs(t *testing.T) { 35 | cmd := &Command{ 36 | Usage: "posargs", 37 | Args: ExactArgs(2), 38 | Run: func(ctx context.Context, args []string) {}, 39 | } 40 | if err := Execute(context.Background(), cmd, []string{"one", "two"}); err != nil { 41 | t.Fatal(err) 42 | } 43 | if err := Execute(context.Background(), cmd, []string{"one", "two", "three"}); err == nil { 44 | t.Fatal("expected error") 45 | } 46 | if err := Execute(context.Background(), cmd, []string{}); err == nil { 47 | t.Fatal("expected error") 48 | } 49 | } 50 | 51 | func TestSubcommands(t *testing.T) { 52 | cmd := &Command{ 53 | Usage: "subcmds", 54 | Run: func(ctx context.Context, args []string) { 55 | fmt.Fprint(IOFrom(ctx), "root") 56 | }, 57 | } 58 | cmd.AddCommand(&Command{ 59 | Usage: "sub1", 60 | Run: func(ctx context.Context, args []string) { 61 | fmt.Fprint(IOFrom(ctx), "sub1") 62 | }, 63 | }) 64 | cmd.AddCommand(&Command{ 65 | Usage: "sub2", 66 | Run: func(ctx context.Context, args []string) { 67 | fmt.Fprint(IOFrom(ctx), "sub2") 68 | }, 69 | }) 70 | 71 | var buf bytes.Buffer 72 | ctx := ContextWithIO(context.Background(), nil, &buf, nil) 73 | 74 | if err := Execute(ctx, cmd, []string{}); err != nil { 75 | t.Fatal(err) 76 | } 77 | if buf.String() != "root" { 78 | t.Fatal("didn't run root cmd") 79 | } 80 | 81 | buf.Reset() 82 | if err := Execute(ctx, cmd, []string{"sub1"}); err != nil { 83 | t.Fatal(err) 84 | } 85 | if buf.String() != "sub1" { 86 | t.Fatal("didn't run sub1 cmd") 87 | } 88 | 89 | buf.Reset() 90 | if err := Execute(ctx, cmd, []string{"sub2"}); err != nil { 91 | t.Fatal(err) 92 | } 93 | if buf.String() != "sub2" { 94 | t.Fatal("didn't run sub2 cmd") 95 | } 96 | } 97 | 98 | func TestFlags(t *testing.T) { 99 | var ( 100 | boolFlag bool 101 | stringFlag string 102 | intFlag int 103 | ) 104 | cmd := &Command{ 105 | Usage: "flags", 106 | Run: func(ctx context.Context, args []string) {}, 107 | } 108 | cmd.Flags().BoolVar(&boolFlag, "b", false, "bool value") 109 | cmd.Flags().StringVar(&stringFlag, "string", "", "string value") 110 | cmd.Flags().IntVar(&intFlag, "int", 0, "int value") 111 | 112 | ctx := context.Background() 113 | if err := Execute(ctx, cmd, []string{"-b=true", "-string", "STRING", "-int=100"}); err != nil { 114 | t.Fatal(err) 115 | } 116 | if boolFlag != true || stringFlag != "STRING" || intFlag != 100 { 117 | t.Fatal("unexpected flag value") 118 | } 119 | } 120 | 121 | func TestVersion(t *testing.T) { 122 | cmd := &Command{ 123 | Version: "1.0", 124 | Usage: "mytest", 125 | Run: func(ctx context.Context, args []string) {}, 126 | } 127 | var buf bytes.Buffer 128 | ctx := ContextWithIO(context.Background(), nil, &buf, nil) 129 | if err := Execute(ctx, cmd, []string{"-v"}); err != nil { 130 | t.Fatal(err) 131 | } 132 | if buf.String() != "1.0\n" { 133 | t.Fatal("unexpected output:", buf.String()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/progrium/qtalk-go/codec" 8 | "github.com/progrium/qtalk-go/mux" 9 | ) 10 | 11 | // RemoteError is an error that has been returned from 12 | // the remote side of the RPC connection. 13 | type RemoteError string 14 | 15 | func (e RemoteError) Error() string { 16 | return fmt.Sprintf("remote: %s", string(e)) 17 | } 18 | 19 | // Client wraps a session and codec to make RPC calls over the session. 20 | type Client struct { 21 | mux.Session 22 | codec codec.Codec 23 | } 24 | 25 | // NewClient takes a session and codec to make a client for making RPC calls. 26 | func NewClient(session mux.Session, codec codec.Codec) *Client { 27 | return &Client{ 28 | Session: session, 29 | codec: codec, 30 | } 31 | } 32 | 33 | // Call makes synchronous calls to the remote selector passing args and putting the reply 34 | // value in reply. Both args and reply can be nil. Args can be a channel of interface{} 35 | // values for asynchronously streaming multiple values from another goroutine, however 36 | // the call will still block until a response is sent. If there is an error making the call 37 | // an error is returned, and if an error is returned by the remote handler a RemoteError 38 | // is returned. 39 | // 40 | // A Response value is also returned for advanced operations. For example, you can check 41 | // if the call is continued, meaning the underlying channel will be kept open for either 42 | // streaming back more results or using the channel as a full duplex byte stream. 43 | func (c *Client) Call(ctx context.Context, selector string, args any, replies ...any) (*Response, error) { 44 | ch, err := c.Session.Open(ctx) 45 | if err != nil { 46 | return nil, err 47 | } 48 | // If the context is cancelled before the call completes, call Close() to 49 | // abort the current operation. 50 | done := make(chan struct{}) 51 | defer close(done) 52 | go func() { 53 | select { 54 | case <-ctx.Done(): 55 | ch.Close() 56 | case <-done: 57 | } 58 | }() 59 | resp, err := call(ctx, ch, c.codec, selector, args, replies...) 60 | if ctxErr := ctx.Err(); ctxErr != nil { 61 | return resp, ctxErr 62 | } 63 | return resp, err 64 | } 65 | 66 | func call(ctx context.Context, ch mux.Channel, cd codec.Codec, selector string, args any, replies ...any) (*Response, error) { 67 | framer := &FrameCodec{Codec: cd} 68 | enc := framer.Encoder(ch) 69 | dec := framer.Decoder(ch) 70 | 71 | // request 72 | err := enc.Encode(CallHeader{ 73 | Selector: selector, 74 | }) 75 | if err != nil { 76 | ch.Close() 77 | return nil, err 78 | } 79 | 80 | argCh, isChan := args.(chan interface{}) 81 | switch { 82 | case isChan: 83 | for arg := range argCh { 84 | if err := enc.Encode(arg); err != nil { 85 | ch.Close() 86 | return nil, err 87 | } 88 | } 89 | default: 90 | if err := enc.Encode(args); err != nil { 91 | ch.Close() 92 | return nil, err 93 | } 94 | } 95 | 96 | // response 97 | var header ResponseHeader 98 | err = dec.Decode(&header) 99 | if err != nil { 100 | ch.Close() 101 | return nil, err 102 | } 103 | 104 | if !header.Continue { 105 | defer ch.Close() 106 | } 107 | 108 | resp := &Response{ 109 | ResponseHeader: header, 110 | Channel: ch, 111 | codec: framer, 112 | } 113 | if len(replies) == 1 { 114 | resp.Reply = replies[0] 115 | } else if len(replies) > 1 { 116 | resp.Reply = replies 117 | } 118 | if resp.Error != nil { 119 | return resp, RemoteError(*resp.Error) 120 | } 121 | 122 | if resp.Reply == nil { 123 | // read into throwaway buffer 124 | var buf []byte 125 | dec.Decode(&buf) 126 | } else { 127 | for _, r := range replies { 128 | if err := dec.Decode(r); err != nil { 129 | return resp, err 130 | } 131 | } 132 | } 133 | 134 | return resp, nil 135 | } 136 | -------------------------------------------------------------------------------- /mux/session_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io/ioutil" 8 | "net" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func init() { 14 | openTimeout = 100 * time.Millisecond 15 | } 16 | 17 | func fatal(err error, t *testing.T) { 18 | t.Helper() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | } 23 | 24 | func TestQmux(t *testing.T) { 25 | l, err := net.Listen("tcp", "127.0.0.1:0") 26 | fatal(err, t) 27 | defer l.Close() 28 | 29 | testComplete := make(chan struct{}) 30 | sessionClosed := make(chan struct{}) 31 | 32 | go func() { 33 | conn, err := l.Accept() 34 | fatal(err, t) 35 | defer conn.Close() 36 | 37 | sess := New(conn) 38 | 39 | ch, err := sess.Open(context.Background()) 40 | fatal(err, t) 41 | b, err := ioutil.ReadAll(ch) 42 | fatal(err, t) 43 | ch.Close() // should already be closed by other end 44 | 45 | ch, err = sess.Accept() 46 | fatal(err, t) 47 | _, err = ch.Write(b) 48 | fatal(err, t) 49 | err = ch.CloseWrite() 50 | fatal(err, t) 51 | 52 | <-testComplete 53 | err = sess.Close() 54 | fatal(err, t) 55 | close(sessionClosed) 56 | }() 57 | 58 | conn, err := net.Dial("tcp", l.Addr().String()) 59 | fatal(err, t) 60 | defer conn.Close() 61 | 62 | sess := New(conn) 63 | 64 | var ch Channel 65 | t.Run("session accept", func(t *testing.T) { 66 | ch, err = sess.Accept() 67 | fatal(err, t) 68 | }) 69 | 70 | t.Run("channel write", func(t *testing.T) { 71 | _, err = ch.Write([]byte("Hello world")) 72 | fatal(err, t) 73 | err = ch.Close() 74 | fatal(err, t) 75 | }) 76 | 77 | t.Run("session open", func(t *testing.T) { 78 | ch, err = sess.Open(context.Background()) 79 | fatal(err, t) 80 | }) 81 | 82 | var b []byte 83 | t.Run("channel read", func(t *testing.T) { 84 | b, err = ioutil.ReadAll(ch) 85 | fatal(err, t) 86 | ch.Close() // should already be closed by other end 87 | }) 88 | 89 | if !bytes.Equal(b, []byte("Hello world")) { 90 | t.Fatalf("unexpected bytes: %s", b) 91 | } 92 | close(testComplete) 93 | <-sessionClosed 94 | } 95 | 96 | func TestSessionOpenClientTimeout(t *testing.T) { 97 | l, err := net.Listen("tcp", "127.0.0.1:0") 98 | fatal(err, t) 99 | defer l.Close() 100 | 101 | conn, err := net.Dial("tcp", l.Addr().String()) 102 | fatal(err, t) 103 | defer conn.Close() 104 | 105 | sess := New(conn) 106 | 107 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 108 | defer cancel() 109 | 110 | ch, err := sess.Open(ctx) 111 | if err != context.DeadlineExceeded { 112 | t.Fatalf("expected DeadlineExceeded, but got: %v", err) 113 | } 114 | if ch != nil { 115 | ch.Close() 116 | } 117 | } 118 | 119 | func TestSessionOpenServerTimeout(t *testing.T) { 120 | l, err := net.Listen("tcp", "127.0.0.1:0") 121 | fatal(err, t) 122 | defer l.Close() 123 | 124 | errCh := make(chan error) 125 | go func() { 126 | conn, err := net.Dial("tcp", l.Addr().String()) 127 | fatal(err, t) 128 | defer conn.Close() 129 | 130 | sess := New(conn) 131 | defer sess.Close() 132 | 133 | _, err = sess.Open(context.Background()) 134 | errCh <- err 135 | }() 136 | 137 | conn, err := l.Accept() 138 | fatal(err, t) 139 | defer conn.Close() 140 | 141 | sess := New(conn) 142 | defer sess.Close() 143 | 144 | if <-errCh == nil { 145 | t.Errorf("expected open to fail when listener doesn't call Accept") 146 | } 147 | fatal(sess.Close(), t) 148 | } 149 | 150 | func TestSessionWait(t *testing.T) { 151 | l, err := net.Listen("tcp", "127.0.0.1:0") 152 | fatal(err, t) 153 | defer l.Close() 154 | 155 | conn, err := net.Dial("tcp", l.Addr().String()) 156 | fatal(err, t) 157 | defer conn.Close() 158 | 159 | sess := New(conn) 160 | fatal(sess.Close(), t) 161 | // wait should return immediately since the connection was closed 162 | err = sess.Wait() 163 | var netErr net.Error 164 | if !errors.As(err, &netErr) { 165 | t.Fatalf("expected a network error, but got: %v", err) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /x/cbor/mux/session_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io/ioutil" 8 | "net" 9 | "testing" 10 | "time" 11 | 12 | "github.com/progrium/qtalk-go/mux" 13 | ) 14 | 15 | func init() { 16 | acceptTimeout = 100 * time.Millisecond 17 | } 18 | 19 | func fatal(err error, t *testing.T) { 20 | t.Helper() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | 26 | func TestCmux(t *testing.T) { 27 | l, err := net.Listen("tcp", "127.0.0.1:0") 28 | fatal(err, t) 29 | defer l.Close() 30 | 31 | testComplete := make(chan struct{}) 32 | sessionClosed := make(chan struct{}) 33 | 34 | go func() { 35 | conn, err := l.Accept() 36 | fatal(err, t) 37 | defer conn.Close() 38 | 39 | sess := New(conn) 40 | 41 | ch, err := sess.Open(context.Background()) 42 | fatal(err, t) 43 | 44 | b, err := ioutil.ReadAll(ch) 45 | fatal(err, t) 46 | ch.Close() // should already be closed by other end 47 | 48 | ch, err = sess.Accept() 49 | fatal(err, t) 50 | 51 | _, err = ch.Write(b) 52 | fatal(err, t) 53 | 54 | err = ch.CloseWrite() 55 | fatal(err, t) 56 | 57 | <-testComplete 58 | err = sess.Close() 59 | fatal(err, t) 60 | close(sessionClosed) 61 | }() 62 | 63 | conn, err := net.Dial("tcp", l.Addr().String()) 64 | fatal(err, t) 65 | defer conn.Close() 66 | 67 | sess := New(conn) 68 | 69 | var ch mux.Channel 70 | t.Run("session accept", func(t *testing.T) { 71 | ch, err = sess.Accept() 72 | fatal(err, t) 73 | }) 74 | 75 | t.Run("channel write", func(t *testing.T) { 76 | _, err = ch.Write([]byte("Hello world")) 77 | fatal(err, t) 78 | err = ch.Close() 79 | fatal(err, t) 80 | }) 81 | 82 | t.Run("session open", func(t *testing.T) { 83 | ch, err = sess.Open(context.Background()) 84 | fatal(err, t) 85 | }) 86 | 87 | var b []byte 88 | t.Run("channel read", func(t *testing.T) { 89 | b, err = ioutil.ReadAll(ch) 90 | fatal(err, t) 91 | ch.Close() // should already be closed by other end 92 | }) 93 | 94 | if !bytes.Equal(b, []byte("Hello world")) { 95 | t.Fatalf("unexpected bytes: %s", b) 96 | } 97 | close(testComplete) 98 | <-sessionClosed 99 | } 100 | 101 | func TestSessionOpenClientTimeout(t *testing.T) { 102 | l, err := net.Listen("tcp", "127.0.0.1:0") 103 | fatal(err, t) 104 | defer l.Close() 105 | 106 | conn, err := net.Dial("tcp", l.Addr().String()) 107 | fatal(err, t) 108 | defer conn.Close() 109 | 110 | sess := New(conn) 111 | 112 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 113 | defer cancel() 114 | 115 | ch, err := sess.Open(ctx) 116 | if err != context.DeadlineExceeded { 117 | t.Fatalf("expected DeadlineExceeded, but got: %v", err) 118 | } 119 | if ch != nil { 120 | ch.Close() 121 | } 122 | } 123 | 124 | func TestSessionOpenServerTimeout(t *testing.T) { 125 | l, err := net.Listen("tcp", "127.0.0.1:0") 126 | fatal(err, t) 127 | defer l.Close() 128 | 129 | errCh := make(chan error) 130 | go func() { 131 | conn, err := net.Dial("tcp", l.Addr().String()) 132 | fatal(err, t) 133 | defer conn.Close() 134 | 135 | sess := New(conn) 136 | defer sess.Close() 137 | 138 | _, err = sess.Open(context.Background()) 139 | errCh <- err 140 | }() 141 | 142 | conn, err := l.Accept() 143 | fatal(err, t) 144 | defer conn.Close() 145 | 146 | sess := New(conn) 147 | defer sess.Close() 148 | 149 | if <-errCh == nil { 150 | t.Errorf("expected open to fail when listener doesn't call Accept") 151 | } 152 | fatal(sess.Close(), t) 153 | } 154 | 155 | func TestSessionWait(t *testing.T) { 156 | l, err := net.Listen("tcp", "127.0.0.1:0") 157 | fatal(err, t) 158 | defer l.Close() 159 | 160 | conn, err := net.Dial("tcp", l.Addr().String()) 161 | fatal(err, t) 162 | defer conn.Close() 163 | 164 | sess := New(conn) 165 | fatal(sess.Close(), t) 166 | // wait should return immediately since the connection was closed 167 | err = sess.Wait() 168 | var netErr net.Error 169 | if !errors.As(err, &netErr) { 170 | t.Fatalf("expected a network error, but got: %v", err) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /cmd/qtalk/check.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | "github.com/progrium/qtalk-go/cmd/qtalk/cli" 15 | "github.com/progrium/qtalk-go/codec" 16 | "github.com/progrium/qtalk-go/fn" 17 | "github.com/progrium/qtalk-go/interop" 18 | "github.com/progrium/qtalk-go/mux" 19 | "github.com/progrium/qtalk-go/rpc" 20 | cbor "github.com/progrium/qtalk-go/x/cbor/codec" 21 | qquic "github.com/progrium/qtalk-go/x/quic" 22 | "github.com/quic-go/quic-go" 23 | ) 24 | 25 | var vals = []any{ 26 | 100, 27 | true, 28 | "hello", 29 | map[string]any{"foo": "bar"}, 30 | []any{1, 2, 3}, 31 | } 32 | 33 | var checkCmd = &cli.Command{ 34 | Usage: "check", 35 | Short: "check interop", 36 | Run: func(ctx context.Context, args []string) { 37 | log.SetOutput(os.Stderr) 38 | 39 | var c codec.Codec = cbor.CBORCodec{} 40 | if os.Getenv("QTALK_CODEC") == "json" { 41 | log.Println("* Using JSON codec") 42 | c = codec.JSONCodec{} 43 | } 44 | 45 | var cmd *exec.Cmd 46 | var sess mux.Session 47 | 48 | if len(args) == 0 { 49 | // self check 50 | path, err := os.Executable() 51 | fatal(err) 52 | cmd = exec.Command(path, "interop") 53 | } else if !strings.HasPrefix(args[0], "udp://") { 54 | // check against subprocess 55 | path, err := exec.LookPath("sh") 56 | fatal(err) 57 | cmd = exec.Command(path, "-c", args[0]) 58 | } 59 | 60 | if cmd != nil { 61 | cmd.Stderr = os.Stderr 62 | wc, err := cmd.StdinPipe() 63 | if err != nil { 64 | fatal(err) 65 | } 66 | rc, err := cmd.StdoutPipe() 67 | if err != nil { 68 | fatal(err) 69 | } 70 | sess, err = mux.DialIO(wc, rc) 71 | if err != nil { 72 | fatal(err) 73 | } 74 | if err := cmd.Start(); err != nil { 75 | fatal(err) 76 | } 77 | defer func() { 78 | cmd.Process.Signal(os.Interrupt) 79 | cmd.Wait() 80 | }() 81 | } else { 82 | // check against remote quic endpoint 83 | cfg := defaultTLSConfig.Clone() 84 | cfg.InsecureSkipVerify = true 85 | conn, err := quic.DialAddr(strings.TrimPrefix(args[0], "udp://"), cfg, nil) 86 | fatal(err) 87 | sess = qquic.New(conn) 88 | } 89 | 90 | defer sess.Close() 91 | 92 | srv := rpc.Server{ 93 | Handler: fn.HandlerFrom(interop.CallbackService{}), 94 | Codec: c, 95 | } 96 | go srv.Respond(sess, nil) 97 | 98 | caller := rpc.NewClient(sess, c) 99 | var ret any 100 | var err error 101 | 102 | // Error check 103 | _, err = caller.Call(ctx, "Error", "test", nil) 104 | if err == nil { 105 | log.Fatal("expected error") 106 | } 107 | fmt.Println("Error:", strings.TrimPrefix(err.Error(), "remote: ")) 108 | _, err = caller.Call(ctx, "BadSelector", "test", nil) 109 | if err == nil { 110 | log.Fatal("expected error") 111 | } 112 | fmt.Println("Error:", strings.TrimPrefix(err.Error(), "remote: ")) 113 | 114 | // Unary check 115 | for _, v := range vals { 116 | _, err = caller.Call(ctx, "Unary", v, &ret) 117 | fatal(err) 118 | fmt.Println("Unary:", v, ret) 119 | } 120 | 121 | // Stream check 122 | resp, err := caller.Call(ctx, "Stream", nil, nil) 123 | fatal(err) 124 | go func() { 125 | for _, v := range vals { 126 | fatal(resp.Send(v)) 127 | } 128 | fatal(resp.Channel.CloseWrite()) 129 | }() 130 | for { 131 | err = resp.Receive(&ret) 132 | if err != nil { 133 | break 134 | } 135 | fmt.Println("Stream:", ret) 136 | } 137 | 138 | // Bytes check 139 | // 1 byte, 1kb, 1mb 140 | for _, v := range []int{1, 1024, 1 << 20} { 141 | data := make([]byte, v) 142 | rand.Read(data) 143 | resp, err = caller.Call(ctx, "Bytes", nil, nil) 144 | fatal(err) 145 | var buf bytes.Buffer 146 | go func() { 147 | io.Copy(resp.Channel, bytes.NewBuffer(data)) 148 | resp.Channel.CloseWrite() 149 | }() 150 | io.Copy(&buf, resp.Channel) 151 | if buf.Len() != len(data) { 152 | log.Fatal("byte stream buffer does not match") 153 | } 154 | fmt.Println("Bytes:", buf.Len()) 155 | } 156 | }, 157 | } 158 | -------------------------------------------------------------------------------- /x/cbor/mux/session.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/fxamacker/cbor/v2" 12 | "github.com/progrium/qtalk-go/mux" 13 | ) 14 | 15 | var ( 16 | // timeout for queuing a new channel to be `Accept`ed 17 | // use a `var` so that this can be overridden in tests 18 | acceptTimeout = 30 * time.Second 19 | ) 20 | 21 | type session struct { 22 | t io.ReadWriteCloser 23 | 24 | chanMu sync.Mutex 25 | chans map[uint32]*channel 26 | chanCounter uint32 27 | 28 | enc *cbor.Encoder 29 | dec *cbor.Decoder 30 | 31 | inbox chan mux.Channel 32 | 33 | errCond *sync.Cond 34 | err error 35 | closeCh chan bool 36 | } 37 | 38 | // NewSession returns a session that runs over the given transport. 39 | func New(t io.ReadWriteCloser) mux.Session { 40 | if t == nil { 41 | return nil 42 | } 43 | s := &session{ 44 | t: t, 45 | enc: cbor.NewEncoder(t), 46 | dec: cbor.NewDecoder(t), 47 | inbox: make(chan mux.Channel), 48 | chans: make(map[uint32]*channel), 49 | errCond: sync.NewCond(new(sync.Mutex)), 50 | closeCh: make(chan bool, 1), 51 | } 52 | go s.loop() 53 | return s 54 | } 55 | 56 | // Close closes the underlying transport. 57 | func (s *session) Close() error { 58 | s.t.Close() 59 | return nil 60 | } 61 | 62 | // Wait blocks until the transport has shut down, and returns the 63 | // error causing the shutdown. 64 | func (s *session) Wait() error { 65 | s.errCond.L.Lock() 66 | defer s.errCond.L.Unlock() 67 | for s.err == nil { 68 | s.errCond.Wait() 69 | } 70 | return s.err 71 | } 72 | 73 | // Accept waits for and returns the next incoming channel. 74 | func (s *session) Accept() (mux.Channel, error) { 75 | select { 76 | case ch := <-s.inbox: 77 | return ch, nil 78 | case <-s.closeCh: 79 | return nil, io.EOF 80 | } 81 | } 82 | 83 | // Open establishes a new channel with the other end. 84 | func (s *session) Open(ctx context.Context) (mux.Channel, error) { 85 | ch := s.newChannel() 86 | if err := s.enc.Encode(Frame{ 87 | Type: channelOpen, 88 | SenderID: ch.localId, 89 | }); err != nil { 90 | return nil, err 91 | } 92 | 93 | var f Frame 94 | var ok bool 95 | 96 | select { 97 | case <-ctx.Done(): 98 | return nil, ctx.Err() 99 | case f, ok = <-ch.frames: 100 | if !ok { 101 | // channel was closed before open got a response, 102 | // typically meaning the session/conn was closed. 103 | return nil, net.ErrClosed 104 | } 105 | } 106 | 107 | switch f.Type { 108 | case channelOpenConfirm: 109 | return ch, nil 110 | case channelOpenFailure: 111 | return nil, fmt.Errorf("cmux: channel open failed on remote side") 112 | default: 113 | return nil, fmt.Errorf("cmux: unexpected packet in response to channel open: %v", f) 114 | } 115 | } 116 | 117 | func (s *session) newChannel() *channel { 118 | ch := &channel{ 119 | pending: newBuffer(), 120 | frames: make(chan Frame, 0), 121 | session: s, 122 | } 123 | s.chanMu.Lock() 124 | s.chanCounter++ 125 | ch.localId = s.chanCounter 126 | s.chans[ch.localId] = ch 127 | s.chanMu.Unlock() 128 | return ch 129 | } 130 | 131 | // loop runs the connection machine. It will process packets until an 132 | // error is encountered. To synchronize on loop exit, use session.Wait. 133 | func (s *session) loop() { 134 | var err error 135 | for err == nil { 136 | err = s.onePacket() 137 | } 138 | //log.Println(err) 139 | 140 | s.chanMu.Lock() 141 | for _, ch := range s.chans { 142 | ch.close() 143 | } 144 | s.chans = make(map[uint32]*channel) 145 | s.chanCounter = 0 146 | s.chanMu.Unlock() 147 | 148 | s.t.Close() 149 | s.closeCh <- true 150 | 151 | s.errCond.L.Lock() 152 | s.err = err 153 | s.errCond.Broadcast() 154 | s.errCond.L.Unlock() 155 | } 156 | 157 | // onePacket reads and processes one packet. 158 | func (s *session) onePacket() (err error) { 159 | var f Frame 160 | err = s.dec.Decode(&f) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | if f.Type == channelOpen { 166 | c := s.newChannel() 167 | c.remoteId = f.SenderID 168 | t := time.NewTimer(acceptTimeout) 169 | defer t.Stop() 170 | select { 171 | case s.inbox <- c: 172 | return s.enc.Encode(Frame{ 173 | Type: channelOpenConfirm, 174 | ChannelID: c.remoteId, 175 | SenderID: c.localId, 176 | }) 177 | case <-t.C: 178 | return s.enc.Encode(Frame{ 179 | Type: channelOpenFailure, 180 | ChannelID: f.SenderID, 181 | }) 182 | } 183 | } 184 | 185 | ch, ok := s.chans[f.ChannelID] 186 | if !ok { 187 | return fmt.Errorf("cmux: unknown channel %d", f.ChannelID) 188 | } 189 | return ch.handle(f) 190 | } 191 | -------------------------------------------------------------------------------- /x/webrtc/webrtc.go: -------------------------------------------------------------------------------- 1 | package webrtc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pion/datachannel" 9 | "github.com/pion/webrtc/v3" 10 | "github.com/progrium/qtalk-go/mux" 11 | ) 12 | 13 | var rtcapi = func() *webrtc.API { 14 | s := webrtc.SettingEngine{} 15 | s.DetachDataChannels() 16 | return webrtc.NewAPI(webrtc.WithSettingEngine(s)) 17 | }() 18 | 19 | func New(peer *webrtc.PeerConnection) mux.Session { 20 | // we probably want client to make an offer, then we provide an answer on a 21 | // new peer connection 22 | // maybe maintain a pool of peer connections to have ready to connect? 23 | 24 | // wait until we get a connection before returning? 25 | channels := make(chan *webrtc.DataChannel) 26 | peer.OnDataChannel(func(d *webrtc.DataChannel) { 27 | d.OnOpen(func() { 28 | // should we use the meta channel for control signals, or close it? 29 | if d.Label() == "qtalk" { 30 | channels <- d 31 | } 32 | }) 33 | }) 34 | return &session{peer, channels, make(chan struct{})} 35 | } 36 | 37 | // let's start with simpler version where we make one offer and wait for an 38 | // answer 39 | 40 | // need to establish a single channel for the answer and exchanging ICE 41 | // candidates 42 | 43 | // but we can wait for ICE to complete, then print the local description 44 | 45 | func Offer(cfg webrtc.Configuration) (*webrtc.PeerConnection, error) { 46 | peer, err := rtcapi.NewPeerConnection(cfg) 47 | if err != nil { 48 | return nil, err 49 | } 50 | // WebRTC requires having at least one channel before creating the offer. 51 | // For now this is unused, but we could use this as an internal control 52 | // channel. Maybe we can close it after the connection is established? 53 | _, err = peer.CreateDataChannel("qtalk-meta", nil) 54 | if err != nil { 55 | peer.Close() 56 | return nil, err 57 | } 58 | offer, err := peer.CreateOffer(nil) 59 | if err != nil { 60 | peer.Close() 61 | return nil, err 62 | } 63 | if err := peer.SetLocalDescription(offer); err != nil { 64 | peer.Close() 65 | return nil, err 66 | } 67 | return peer, nil 68 | } 69 | 70 | func Answer(cfg webrtc.Configuration, offer webrtc.SessionDescription) (*webrtc.PeerConnection, error) { 71 | peer, err := rtcapi.NewPeerConnection(cfg) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if err := peer.SetRemoteDescription(offer); err != nil { 76 | peer.Close() 77 | return nil, err 78 | } 79 | answer, err := peer.CreateAnswer(nil) 80 | if err != nil { 81 | peer.Close() 82 | return nil, err 83 | } 84 | if err := peer.SetLocalDescription(answer); err != nil { 85 | peer.Close() 86 | return nil, err 87 | } 88 | return peer, nil 89 | } 90 | 91 | type session struct { 92 | peer *webrtc.PeerConnection 93 | channels chan *webrtc.DataChannel 94 | done chan struct{} 95 | } 96 | 97 | func (s *session) Close() error { 98 | return s.peer.Close() 99 | } 100 | 101 | func (s *session) Accept() (mux.Channel, error) { 102 | // TODO select on done channel 103 | select { 104 | case ch := <-s.channels: 105 | return newChannel(ch) 106 | case <-s.done: 107 | return nil, fmt.Errorf("session closed") 108 | } 109 | } 110 | 111 | func (s *session) Open(ctx context.Context) (mux.Channel, error) { 112 | d, err := s.peer.CreateDataChannel("qtalk", nil) 113 | if err != nil { 114 | return nil, err 115 | } 116 | opened := make(chan struct{}) 117 | d.OnOpen(func() { close(opened) }) 118 | select { 119 | case <-opened: 120 | return newChannel(d) 121 | case <-ctx.Done(): 122 | d.Close() 123 | return nil, ctx.Err() 124 | } 125 | } 126 | 127 | func (s *session) Wait() error { 128 | panic("not implemented") 129 | } 130 | 131 | func newChannel(ch *webrtc.DataChannel) (mux.Channel, error) { 132 | rwc, err := ch.Detach() 133 | if err != nil { 134 | return nil, err 135 | } 136 | return &channel{ 137 | // id is assigned before calling OnOpen, so we expect it to be non-nil 138 | id: uint32(*ch.ID()), 139 | ch: ch, 140 | rwc: rwc, 141 | }, nil 142 | } 143 | 144 | type channel struct { 145 | id uint32 146 | ch *webrtc.DataChannel 147 | rwc datachannel.ReadWriteCloser 148 | gotEOF bool 149 | closedWrite bool 150 | } 151 | 152 | func (c *channel) ID() uint32 { 153 | return c.id 154 | } 155 | 156 | func (c *channel) Read(p []byte) (int, error) { 157 | if c.gotEOF { 158 | return 0, io.EOF 159 | } 160 | n, isString, err := c.rwc.ReadDataChannel(p) 161 | if err != nil { 162 | return n, err 163 | } 164 | if isString && string(p[:n]) == "EOF" { 165 | return 0, io.EOF 166 | } 167 | return n, nil 168 | } 169 | 170 | func (c *channel) Write(p []byte) (int, error) { 171 | if c.closedWrite { 172 | return 0, io.ErrClosedPipe 173 | } 174 | return c.rwc.Write(p) 175 | } 176 | 177 | func (c *channel) Close() error { 178 | return c.rwc.Close() 179 | } 180 | 181 | func (c *channel) CloseWrite() error { 182 | _, err := c.rwc.WriteDataChannel([]byte("EOF"), true) 183 | if err != nil { 184 | return err 185 | } 186 | c.closedWrite = true 187 | return nil 188 | } 189 | 190 | type Signaler interface { 191 | } 192 | -------------------------------------------------------------------------------- /rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/progrium/qtalk-go/codec" 7 | "github.com/progrium/qtalk-go/mux" 8 | ) 9 | 10 | // A Caller is able to perform remote calls. 11 | // 12 | // Call makes synchronous calls to the remote selector passing args and putting the reply 13 | // value(s) in reply. Both args and reply can be nil. Args can be a channel of interface{} 14 | // values for asynchronously streaming multiple values from another goroutine, however 15 | // the call will still block until a response is sent. If there is an error making the call 16 | // an error is returned, and if an error is returned by the remote handler a RemoteError 17 | // is returned. Multiple reply parameters can be provided in order to receive multi-valued 18 | // returns from the remote call. 19 | // 20 | // A Response value is also returned for advanced operations. For example, you can check 21 | // if the call is continued, meaning the underlying channel will be kept open for either 22 | // streaming back more results or using the channel as a full duplex byte stream. 23 | type Caller interface { 24 | Call(ctx context.Context, selector string, params any, reply ...any) (*Response, error) 25 | } 26 | 27 | // CallHeader is the first value encoded over the channel to make a call. 28 | type CallHeader struct { 29 | Selector string 30 | } 31 | 32 | // Call is used on the responding side of a call and is passed to the handler. 33 | // Call has a Caller so it can be used to make calls back to the calling side. 34 | type Call struct { 35 | CallHeader 36 | 37 | Caller Caller 38 | Decoder codec.Decoder 39 | Context context.Context 40 | 41 | mux.Channel 42 | } 43 | 44 | // Receive will decode an incoming value from the underlying channel. It can be 45 | // called more than once when multiple values are expected, but should always be 46 | // called once in a handler. It can be called with nil to discard the value. 47 | func (c *Call) Receive(v interface{}) error { 48 | if v == nil { 49 | var discard []byte 50 | v = &discard 51 | } 52 | return c.Decoder.Decode(v) 53 | } 54 | 55 | // ResponseHeader is the value encoded over the channel to indicate a response. 56 | type ResponseHeader struct { 57 | Error *string 58 | Continue bool // after parsing response, keep stream open for whatever protocol 59 | } 60 | 61 | // Response is used on the calling side to represent a response and allow access 62 | // to the ResponseHeader data, the reply value, the underlying channel, and methods 63 | // to send or receive encoded values over the channel if Continue was set on the 64 | // ResponseHeader. 65 | type Response struct { 66 | ResponseHeader 67 | Reply interface{} 68 | Channel mux.Channel 69 | 70 | codec codec.Codec 71 | } 72 | 73 | // Send encodes a value over the underlying channel if it is still open. 74 | func (r *Response) Send(v interface{}) error { 75 | return r.codec.Encoder(r.Channel).Encode(v) 76 | } 77 | 78 | // Receive decodes a value from the underlying channel if it is still open. 79 | func (r *Response) Receive(v interface{}) error { 80 | return r.codec.Decoder(r.Channel).Decode(v) 81 | } 82 | 83 | // Responder is used by handlers to initiate a response and send values to the caller. 84 | type Responder interface { 85 | // Return sends a return value, which can be an error, and closes the channel. 86 | Return(...any) error 87 | 88 | // Continue sets the response to keep the channel open after sending a return value, 89 | // and returns the underlying channel for you to take control of. If called, you 90 | // become responsible for closing the channel. 91 | Continue(...any) (mux.Channel, error) 92 | 93 | // Send encodes a value over the underlying channel, but does not initiate a response, 94 | // so it must be used after calling Continue. 95 | Send(interface{}) error 96 | } 97 | 98 | type responder struct { 99 | responded bool 100 | header *ResponseHeader 101 | ch mux.Channel 102 | c codec.Codec 103 | } 104 | 105 | func (r *responder) Send(v interface{}) error { 106 | return r.c.Encoder(r.ch).Encode(v) 107 | } 108 | 109 | func (r *responder) Return(v ...any) error { 110 | return r.respond(v, false) 111 | } 112 | 113 | func (r *responder) Continue(v ...any) (mux.Channel, error) { 114 | return r.ch, r.respond(v, true) 115 | } 116 | 117 | func (r *responder) respond(values []any, continue_ bool) error { 118 | r.responded = true 119 | r.header.Continue = continue_ 120 | 121 | // if values is a single error, set values to [nil] 122 | // and put error in header 123 | if len(values) == 1 { 124 | var e error 125 | var ok bool 126 | if e, ok = values[0].(error); ok { 127 | values = []any{nil} 128 | } 129 | if e != nil { 130 | var errStr = e.Error() 131 | r.header.Error = &errStr 132 | } 133 | } 134 | 135 | if err := r.Send(r.header); err != nil { 136 | return err 137 | } 138 | 139 | // The original calling convention expects at least one return, so return 140 | // `nil` if there is no other return value. 141 | if len(values) == 0 { 142 | values = []any{nil} 143 | } 144 | for _, v := range values { 145 | if err := r.Send(v); err != nil { 146 | return err 147 | } 148 | } 149 | 150 | if !continue_ { 151 | return r.ch.Close() 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /cmd/qtalk/cli/help.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strings" 9 | "text/template" 10 | "unicode" 11 | ) 12 | 13 | // HelpFuncs are used by the help templating system. 14 | var HelpFuncs = template.FuncMap{ 15 | "trim": strings.TrimSpace, 16 | "trimRight": func(s string) string { 17 | return strings.TrimRightFunc(s, unicode.IsSpace) 18 | }, 19 | "padRight": func(s string, padding int) string { 20 | template := fmt.Sprintf("%%-%ds", padding) 21 | return fmt.Sprintf(template, s) 22 | }, 23 | } 24 | 25 | // HelpTemplate is a template used to generate help. 26 | var HelpTemplate = `Usage:{{if .Runnable}} 27 | {{.UseLine}}{{end}}{{if .HasDescription}} 28 | 29 | {{.Description}}{{end}}{{if gt (len .Aliases) 0}} 30 | 31 | Aliases: 32 | {{.NameAndAliases}}{{end}}{{if .HasExample}} 33 | 34 | Examples: 35 | {{.Example}}{{end}}{{if .HasSubCommands}} 36 | 37 | Available Commands:{{range .Commands}}{{if (or .Available (eq .Name "help"))}} 38 | {{padRight .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasFlags}} 39 | 40 | Flags: 41 | {{.FlagUsages | trimRight }}{{end}}{{if .HasSubCommands}} 42 | 43 | Use "{{.CommandPath}} [command] -help" for more information about a command.{{end}} 44 | ` 45 | 46 | // CommandHelp wraps a Command to generate help. 47 | type CommandHelp struct { 48 | *Command 49 | } 50 | 51 | // WriteHelp generates help for the command written to an io.Writer. 52 | func (c *CommandHelp) WriteHelp(w io.Writer) error { 53 | t := template.Must(template.New("help").Funcs(HelpFuncs).Parse(HelpTemplate)) 54 | return t.Execute(w, c) 55 | } 56 | 57 | // Runnable determines if the command is itself runnable. 58 | func (c *CommandHelp) Runnable() bool { 59 | return c.Run != nil 60 | } 61 | 62 | func (c *CommandHelp) HasDescription() bool { 63 | return c.Short != "" || c.Long != "" 64 | } 65 | 66 | func (c *CommandHelp) Description() string { 67 | if c.Long != "" { 68 | return c.Long 69 | } 70 | return c.Short 71 | } 72 | 73 | // Available determines if a command is available as a non-help command (this includes all non hidden commands). 74 | func (c *CommandHelp) Available() bool { 75 | if c.Hidden { 76 | return false 77 | } 78 | if c.Runnable() || c.HasSubCommands() { 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | // HasSubCommands determines if a command has available sub commands that need to be 85 | // shown in the usage/help default template under 'available commands'. 86 | func (c *CommandHelp) HasSubCommands() bool { 87 | for _, sub := range c.commands { 88 | if (&CommandHelp{sub}).Available() { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | // NameAndAliases returns a list of the command name and all aliases. 96 | func (c *CommandHelp) NameAndAliases() string { 97 | return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ") 98 | } 99 | 100 | // HasExample determines if the command has example. 101 | func (c *CommandHelp) HasExample() bool { 102 | return len(c.Example) > 0 103 | } 104 | 105 | // Commands returns any subcommands as CommandHelp values. 106 | func (c *CommandHelp) Commands() (cmds []*CommandHelp) { 107 | for _, cmd := range c.commands { 108 | cmds = append(cmds, &CommandHelp{cmd}) 109 | } 110 | return 111 | } 112 | 113 | // NamePadding returns padding for the name. 114 | func (c *CommandHelp) NamePadding() int { 115 | // TODO: consider making this dynamic, based on length of all sibling commands 116 | return 16 117 | } 118 | 119 | // HasFlags checks if the command contains flags. 120 | func (c *CommandHelp) HasFlags() bool { 121 | n := 0 122 | c.Flags().VisitAll(func(f *flag.Flag) { 123 | n++ 124 | }) 125 | return n > 0 126 | } 127 | 128 | // FlagUsages creates a string for flag usage help. 129 | func (c *CommandHelp) FlagUsages() string { 130 | var sb strings.Builder 131 | c.Flags().VisitAll(func(f *flag.Flag) { 132 | fmt.Fprintf(&sb, " -%s", f.Name) // Two spaces before -; see next two comments. 133 | name, usage := flag.UnquoteUsage(f) 134 | if len(name) > 0 { 135 | sb.WriteString(" ") 136 | sb.WriteString(name) 137 | } 138 | // Boolean flags of one ASCII letter are so common we 139 | // treat them specially, putting their usage on the same line. 140 | if sb.Len() <= 4 { // space, space, '-', 'x'. 141 | sb.WriteString("\t") 142 | } else { 143 | // Four spaces before the tab triggers good alignment 144 | // for both 4- and 8-space tab stops. 145 | sb.WriteString("\n \t") 146 | } 147 | sb.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) 148 | f.Usage = "" 149 | if !isZeroValue(f, f.DefValue) { 150 | typ, _ := flag.UnquoteUsage(f) 151 | if typ == "string" { 152 | // put quotes on the value 153 | fmt.Fprintf(&sb, " (default %q)", f.DefValue) 154 | } else { 155 | fmt.Fprintf(&sb, " (default %v)", f.DefValue) 156 | } 157 | } 158 | sb.WriteString("\n") 159 | }) 160 | return sb.String() 161 | } 162 | 163 | // isZeroValue determines whether the string represents the zero 164 | // value for a flag. 165 | func isZeroValue(f *flag.Flag, value string) bool { 166 | // Build a zero value of the flag's Value type, and see if the 167 | // result of calling its String method equals the value passed in. 168 | // This works unless the Value type is itself an interface type. 169 | typ := reflect.TypeOf(f.Value) 170 | var z reflect.Value 171 | if typ.Kind() == reflect.Ptr { 172 | z = reflect.New(typ.Elem()) 173 | } else { 174 | z = reflect.Zero(typ) 175 | } 176 | return value == z.Interface().(flag.Value).String() 177 | } 178 | -------------------------------------------------------------------------------- /fn/handler.go: -------------------------------------------------------------------------------- 1 | package fn 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/progrium/qtalk-go/rpc" 10 | ) 11 | 12 | // HandlerFrom uses reflection to return a handler from either a function or 13 | // methods from a struct. When a struct is used, HandlerFrom creates a RespondMux 14 | // registering each method as a handler using its method name. From there, methods 15 | // are treated just like functions. 16 | // 17 | // The registered methods can be limited by providing an interface type parameter: 18 | // 19 | // h := HandlerFrom[interface{ 20 | // OnlyTheseMethods() 21 | // WillBeRegistered() 22 | // }](myHandlerImplementation) 23 | // 24 | // If a struct method matches the HandlerFunc signature, the method will be called 25 | // directly with the handler arguments. Otherwise it will be wrapped as described below. 26 | // 27 | // Function handlers expect an array to use as arguments. If the incoming argument 28 | // array is too large or too small, the handler returns an error. Functions can opt-in 29 | // to take a final Call pointer argument, allowing the handler to give it the Call value 30 | // being processed. Functions can return nothing which the handler returns as nil, or 31 | // a single value which can be an error, or two values where one value is an error. 32 | // In the latter case, the value is returned if the error is nil, otherwise just the 33 | // error is returned. Handlers based on functions that return more than two values will 34 | // simply ignore the remaining values. 35 | // 36 | // Structs that implement the Handler interface will be added as a catch-all handler 37 | // along with their individual methods. This lets you implement dynamic methods. 38 | func HandlerFrom[T any](v T) rpc.Handler { 39 | rv := reflect.Indirect(reflect.ValueOf(v)) 40 | switch rv.Type().Kind() { 41 | case reflect.Func: 42 | return fromFunc(reflect.ValueOf(v)) 43 | case reflect.Struct: 44 | t := reflect.TypeOf((*T)(nil)).Elem() 45 | return fromMethods(v, t) 46 | default: 47 | panic("must be func or struct") 48 | } 49 | } 50 | 51 | // Args is the expected argument value for calls made to HandlerFrom handlers. 52 | // Since it is just a slice of empty interface values, you can alternatively use 53 | // more specific slice types ([]int{}, etc) if all arguments are of the same type. 54 | type Args []any 55 | 56 | var handlerFuncType = reflect.TypeOf((*rpc.HandlerFunc)(nil)).Elem() 57 | 58 | func fromMethods(rcvr interface{}, t reflect.Type) rpc.Handler { 59 | // If `t` is an interface, `Convert()` wraps the value with that interface 60 | // type. This makes sure that the Method(i) indexes match for getting both the 61 | // name and implementation. 62 | rcvrval := reflect.ValueOf(rcvr).Convert(t) 63 | mux := rpc.NewRespondMux() 64 | for i := 0; i < t.NumMethod(); i++ { 65 | m := rcvrval.Method(i) 66 | var h rpc.Handler 67 | if m.CanConvert(handlerFuncType) { 68 | h = m.Convert(handlerFuncType).Interface().(rpc.HandlerFunc) 69 | } else { 70 | h = fromFunc(m) 71 | } 72 | mux.Handle(t.Method(i).Name, h) 73 | } 74 | h, ok := rcvr.(rpc.Handler) 75 | if ok { 76 | mux.Handle("/", h) 77 | } 78 | return mux 79 | } 80 | 81 | var callRef = reflect.TypeOf((*rpc.Call)(nil)) 82 | 83 | func fromFunc(fn reflect.Value) rpc.Handler { 84 | fntyp := fn.Type() 85 | // if the last argument in fn is an rpc.Call, add our call to fnParams 86 | expectsCallParam := fntyp.NumIn() > 0 && fntyp.In(fntyp.NumIn()-1) == callRef 87 | 88 | return rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) { 89 | defer func() { 90 | if p := recover(); p != nil { 91 | r.Return(fmt.Errorf("panic: %s [%s]", p, identifyPanic())) 92 | } 93 | }() 94 | 95 | var params []any 96 | if err := c.Receive(¶ms); err != nil { 97 | r.Return(fmt.Errorf("fn: args: %s", err.Error())) 98 | return 99 | } 100 | if expectsCallParam { 101 | params = append(params, c) 102 | } 103 | ret, err := Call(fn.Interface(), params) 104 | if err != nil { 105 | r.Return(err) 106 | return 107 | } 108 | r.Return(ret...) 109 | }) 110 | } 111 | 112 | // ensureType ensures a value is converted to the expected 113 | // defined type from a convertable underlying type 114 | func ensureType(v reflect.Value, t reflect.Type) reflect.Value { 115 | nv := v 116 | if v.Type().Kind() == reflect.Slice && v.Type().Elem() != t { 117 | switch t.Kind() { 118 | case reflect.Array: 119 | nv = reflect.Indirect(reflect.New(t)) 120 | for i := 0; i < v.Len(); i++ { 121 | vv := reflect.ValueOf(v.Index(i).Interface()) 122 | nv.Index(i).Set(vv.Convert(nv.Type().Elem())) 123 | } 124 | case reflect.Slice: 125 | nv = reflect.MakeSlice(t, 0, 0) 126 | for i := 0; i < v.Len(); i++ { 127 | vv := reflect.ValueOf(v.Index(i).Interface()) 128 | nv = reflect.Append(nv, vv.Convert(nv.Type().Elem())) 129 | } 130 | default: 131 | panic("unable to convert slice to non-array, non-slice type") 132 | } 133 | } 134 | if v.Type() != t { 135 | nv = nv.Convert(t) 136 | } 137 | return nv 138 | } 139 | 140 | func identifyPanic() string { 141 | var name, file string 142 | var line int 143 | var pc [16]uintptr 144 | 145 | n := runtime.Callers(3, pc[:]) 146 | for _, pc := range pc[:n] { 147 | fn := runtime.FuncForPC(pc) 148 | if fn == nil { 149 | continue 150 | } 151 | file, line = fn.FileLine(pc) 152 | name = fn.Name() 153 | if !strings.HasPrefix(name, "runtime.") { 154 | break 155 | } 156 | } 157 | 158 | switch { 159 | case name != "": 160 | return fmt.Sprintf("%v:%v", name, line) 161 | case file != "": 162 | return fmt.Sprintf("%v:%v", file, line) 163 | } 164 | 165 | return fmt.Sprintf("pc:%x", pc) 166 | } 167 | -------------------------------------------------------------------------------- /mux/session.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/progrium/qtalk-go/mux/frame" 12 | ) 13 | 14 | const ( 15 | minPacketLength = 9 16 | maxPacketLength = 1 << 31 17 | 18 | // channelMaxPacket contains the maximum number of bytes that will be 19 | // sent in a single packet. 20 | channelMaxPacket = 1 << 24 // ~16MB, arbitrary 21 | // We follow OpenSSH here. 22 | channelWindowSize = 64 * channelMaxPacket 23 | 24 | // chanSize sets the amount of buffering qmux connections. This is 25 | // primarily for testing: setting chanSize=0 uncovers deadlocks more 26 | // quickly. 27 | chanSize = 16 28 | ) 29 | 30 | var ( 31 | // timeout for queuing a new channel to be `Accept`ed 32 | // use a `var` so that this can be overridden in tests 33 | openTimeout = 30 * time.Second 34 | ) 35 | 36 | // Session is a bi-directional channel muxing session on a given transport. 37 | type Session interface { 38 | io.Closer 39 | Accept() (Channel, error) 40 | Open(ctx context.Context) (Channel, error) 41 | Wait() error 42 | } 43 | 44 | type session struct { 45 | t io.ReadWriteCloser 46 | chans chanList 47 | 48 | enc *frame.Encoder 49 | dec *frame.Decoder 50 | 51 | inbox chan Channel 52 | 53 | errCond *sync.Cond 54 | err error 55 | closeCh chan bool 56 | } 57 | 58 | // NewSession returns a session that runs over the given transport. 59 | func New(t io.ReadWriteCloser) Session { 60 | if t == nil { 61 | return nil 62 | } 63 | s := &session{ 64 | t: t, 65 | enc: frame.NewEncoder(t), 66 | dec: frame.NewDecoder(t), 67 | inbox: make(chan Channel), 68 | errCond: sync.NewCond(new(sync.Mutex)), 69 | closeCh: make(chan bool, 1), 70 | } 71 | go s.loop() 72 | return s 73 | } 74 | 75 | // Close closes the underlying transport. 76 | func (s *session) Close() error { 77 | s.t.Close() 78 | return nil 79 | } 80 | 81 | // Wait blocks until the transport has shut down, and returns the 82 | // error causing the shutdown. 83 | func (s *session) Wait() error { 84 | s.errCond.L.Lock() 85 | defer s.errCond.L.Unlock() 86 | for s.err == nil { 87 | s.errCond.Wait() 88 | } 89 | return s.err 90 | } 91 | 92 | // Accept waits for and returns the next incoming channel. 93 | func (s *session) Accept() (Channel, error) { 94 | select { 95 | case ch := <-s.inbox: 96 | return ch, nil 97 | case <-s.closeCh: 98 | return nil, io.EOF 99 | } 100 | } 101 | 102 | // Open establishes a new channel with the other end. 103 | func (s *session) Open(ctx context.Context) (Channel, error) { 104 | ch := s.newChannel(channelOutbound) 105 | ch.maxIncomingPayload = channelMaxPacket 106 | 107 | if err := s.enc.Encode(frame.OpenMessage{ 108 | WindowSize: ch.myWindow, 109 | MaxPacketSize: ch.maxIncomingPayload, 110 | SenderID: ch.localId, 111 | }); err != nil { 112 | return nil, err 113 | } 114 | 115 | var m frame.Message 116 | 117 | select { 118 | case <-ctx.Done(): 119 | return nil, ctx.Err() 120 | case m = <-ch.msg: 121 | if m == nil { 122 | // channel was closed before open got a response, 123 | // typically meaning the session/conn was closed. 124 | return nil, net.ErrClosed 125 | } 126 | } 127 | 128 | switch msg := m.(type) { 129 | case *frame.OpenConfirmMessage: 130 | return ch, nil 131 | case *frame.OpenFailureMessage: 132 | return nil, fmt.Errorf("qmux: channel open failed on remote side") 133 | default: 134 | return nil, fmt.Errorf("qmux: unexpected packet in response to channel open: %v", msg) 135 | } 136 | } 137 | 138 | func (s *session) newChannel(direction channelDirection) *channel { 139 | ch := &channel{ 140 | remoteWin: window{Cond: sync.NewCond(new(sync.Mutex))}, 141 | myWindow: channelWindowSize, 142 | pending: newBuffer(), 143 | direction: direction, 144 | msg: make(chan frame.Message, chanSize), 145 | session: s, 146 | packetBuf: make([]byte, 0), 147 | } 148 | ch.localId = s.chans.add(ch) 149 | return ch 150 | } 151 | 152 | // loop runs the connection machine. It will process packets until an 153 | // error is encountered. To synchronize on loop exit, use session.Wait. 154 | func (s *session) loop() { 155 | var err error 156 | for err == nil { 157 | err = s.onePacket() 158 | } 159 | 160 | for _, ch := range s.chans.dropAll() { 161 | ch.close() 162 | } 163 | 164 | s.t.Close() 165 | s.closeCh <- true 166 | 167 | s.errCond.L.Lock() 168 | s.err = err 169 | s.errCond.Broadcast() 170 | s.errCond.L.Unlock() 171 | } 172 | 173 | // onePacket reads and processes one packet. 174 | func (s *session) onePacket() error { 175 | var err error 176 | var msg frame.Message 177 | 178 | msg, err = s.dec.Decode() 179 | if err != nil { 180 | return err 181 | } 182 | 183 | id, isChan := msg.Channel() 184 | if !isChan { 185 | return s.handleOpen(msg.(*frame.OpenMessage)) 186 | } 187 | 188 | ch := s.chans.getChan(id) 189 | if ch == nil { 190 | return fmt.Errorf("qmux: invalid channel %d", id) 191 | } 192 | 193 | return ch.handle(msg) 194 | } 195 | 196 | // handleChannelOpen schedules a channel to be Accept()ed. 197 | func (s *session) handleOpen(msg *frame.OpenMessage) error { 198 | if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > maxPacketLength { 199 | return s.enc.Encode(frame.OpenFailureMessage{ 200 | ChannelID: msg.SenderID, 201 | }) 202 | } 203 | 204 | c := s.newChannel(channelInbound) 205 | c.remoteId = msg.SenderID 206 | c.maxRemotePayload = msg.MaxPacketSize 207 | c.remoteWin.add(msg.WindowSize) 208 | c.maxIncomingPayload = channelMaxPacket 209 | t := time.NewTimer(openTimeout) 210 | defer t.Stop() 211 | select { 212 | case s.inbox <- c: 213 | return s.enc.Encode(frame.OpenConfirmMessage{ 214 | ChannelID: c.remoteId, 215 | SenderID: c.localId, 216 | WindowSize: c.myWindow, 217 | MaxPacketSize: c.maxIncomingPayload, 218 | }) 219 | case <-t.C: 220 | return s.enc.Encode(frame.OpenFailureMessage{ 221 | ChannelID: msg.SenderID, 222 | }) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /cmd/qtalk/cli/command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // Command is a command or subcommand that can be run with Execute. 12 | type Command struct { 13 | // Use is the one-line usage message. 14 | // Recommended syntax is as follow: 15 | // [ ] identifies an optional argument. Arguments that are not enclosed in brackets are required. 16 | // ... indicates that you can specify multiple values for the previous argument. 17 | // | indicates mutually exclusive information. You can use the argument to the left of the separator or the 18 | // argument to the right of the separator. You cannot use both arguments in a single use of the command. 19 | // { } delimits a set of mutually exclusive arguments when one of the arguments is required. If the arguments are 20 | // optional, they are enclosed in brackets ([ ]). 21 | // Example: add [-F file | -D dir]... [-f format] 22 | Usage string 23 | 24 | // Short is the short description shown in the 'help' output. 25 | Short string 26 | 27 | // Long is the long message shown in the 'help ' output. 28 | Long string 29 | 30 | // Hidden defines, if this command is hidden and should NOT show up in the list of available commands. 31 | Hidden bool 32 | 33 | // Aliases is an array of aliases that can be used instead of the first word in Use. 34 | Aliases []string 35 | 36 | // Example is examples of how to use the command. 37 | Example string 38 | 39 | // Annotations are key/value pairs that can be used by applications to identify or 40 | // group commands. 41 | Annotations map[string]interface{} 42 | 43 | // Version defines the version for this command. If this value is non-empty and the command does not 44 | // define a "version" flag, a "version" boolean flag will be added to the command and, if specified, 45 | // will print content of the "Version" variable. A shorthand "v" flag will also be added if the 46 | // command does not define one. 47 | Version string 48 | 49 | // Expected arguments 50 | Args PositionalArgs 51 | 52 | // Run is the function that performs the command 53 | Run func(ctx context.Context, args []string) 54 | 55 | commands []*Command 56 | parent *Command 57 | flags *flag.FlagSet 58 | } 59 | 60 | // Flags returns the complete FlagSet that applies to this command. 61 | func (c *Command) Flags() *flag.FlagSet { 62 | if c.flags == nil { 63 | c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) 64 | var null bytes.Buffer 65 | c.flags.SetOutput(&null) 66 | } 67 | return c.flags 68 | } 69 | 70 | // AddCommand adds one or more commands to this parent command. 71 | func (c *Command) AddCommand(sub *Command) { 72 | if sub == c { 73 | panic("command can't be a child of itself") 74 | } 75 | sub.parent = c 76 | c.commands = append(c.commands, sub) 77 | } 78 | 79 | // CommandPath returns the full path to this command. 80 | func (c *Command) CommandPath() string { 81 | if c.parent != nil { 82 | return c.parent.CommandPath() + " " + c.Name() 83 | } 84 | return c.Name() 85 | } 86 | 87 | // UseLine puts out the full usage for a given command (including parents). 88 | func (c *Command) UseLine() string { 89 | use := c.Usage 90 | if use == c.Name() && len(c.commands) > 0 { 91 | use = fmt.Sprintf("%s [command]", c.Name()) 92 | } 93 | if c.parent != nil { 94 | return c.parent.CommandPath() + " " + use 95 | } else { 96 | return use 97 | } 98 | } 99 | 100 | // Name returns the command's name: the first word in the usage line. 101 | func (c *Command) Name() string { 102 | name := c.Usage 103 | i := strings.Index(name, " ") 104 | if i >= 0 { 105 | name = name[:i] 106 | } 107 | return name 108 | } 109 | 110 | // Find the target command given the args and command tree. 111 | // Meant to be run on the highest node. Only searches down. 112 | // Also returns the arguments consumed to reach the command. 113 | func (c *Command) Find(args []string) (cmd *Command, n int) { 114 | cmd = c 115 | if len(args) == 0 { 116 | return 117 | } 118 | var arg string 119 | for n, arg = range args { 120 | if cc := cmd.findSub(arg); cc != nil { 121 | cmd = cc 122 | } else { 123 | return 124 | } 125 | } 126 | n += 1 127 | return 128 | } 129 | 130 | func (c *Command) findSub(name string) *Command { 131 | for _, cmd := range c.commands { 132 | if cmd.Name() == name || hasAlias(cmd, name) { 133 | return cmd 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | func hasAlias(cmd *Command, name string) bool { 140 | for _, a := range cmd.Aliases { 141 | if a == name { 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | 148 | // PositionalArgs is a function type used by the Command Args field 149 | // for detecting whether the arguments match a given expectation. 150 | type PositionalArgs func(cmd *Command, args []string) error 151 | 152 | // MinArgs returns an error if there is not at least N args. 153 | func MinArgs(n int) PositionalArgs { 154 | return func(cmd *Command, args []string) error { 155 | if len(args) < n { 156 | return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) 157 | } 158 | return nil 159 | } 160 | } 161 | 162 | // MaxArgs returns an error if there are more than N args. 163 | func MaxArgs(n int) PositionalArgs { 164 | return func(cmd *Command, args []string) error { 165 | if len(args) > n { 166 | return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args)) 167 | } 168 | return nil 169 | } 170 | } 171 | 172 | // ExactArgs returns an error if there are not exactly n args. 173 | func ExactArgs(n int) PositionalArgs { 174 | return func(cmd *Command, args []string) error { 175 | if len(args) != n { 176 | return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) 177 | } 178 | return nil 179 | } 180 | } 181 | 182 | // RangeArgs returns an error if the number of args is not within the expected range. 183 | func RangeArgs(min int, max int) PositionalArgs { 184 | return func(cmd *Command, args []string) error { 185 | if len(args) < min || len(args) > max { 186 | return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args)) 187 | } 188 | return nil 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /x/quic/quic_test.go: -------------------------------------------------------------------------------- 1 | package quic 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "encoding/pem" 11 | "io/ioutil" 12 | "math/big" 13 | "testing" 14 | "time" 15 | 16 | "github.com/progrium/qtalk-go/mux" 17 | "github.com/quic-go/quic-go" 18 | ) 19 | 20 | func fatal(err error, t *testing.T) { 21 | t.Helper() 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | 27 | func generateTLSConfig() *tls.Config { 28 | key, err := rsa.GenerateKey(rand.Reader, 1024) 29 | if err != nil { 30 | panic(err) 31 | } 32 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 33 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 34 | if err != nil { 35 | panic(err) 36 | } 37 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 38 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 39 | 40 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 41 | if err != nil { 42 | panic(err) 43 | } 44 | cfg := defaultTLSConfig.Clone() 45 | cfg.Certificates = []tls.Certificate{tlsCert} 46 | return cfg 47 | } 48 | 49 | func TestSingleChannelEcho(t *testing.T) { 50 | l, err := quic.ListenAddr("127.0.0.1:0", generateTLSConfig(), nil) 51 | fatal(err, t) 52 | defer l.Close() 53 | 54 | testComplete := make(chan struct{}) 55 | sessionClosed := make(chan struct{}) 56 | 57 | go func() { 58 | conn, err := l.Accept(context.Background()) 59 | fatal(err, t) 60 | // defer conn.Close() 61 | 62 | sess := New(conn) 63 | 64 | ch, err := sess.Open(context.Background()) 65 | fatal(err, t) 66 | b, err := ioutil.ReadAll(ch) 67 | fatal(err, t) 68 | _, err = ch.Write(b) 69 | fatal(err, t) 70 | err = ch.CloseWrite() 71 | ch.Close() // should already be closed by other end 72 | 73 | <-testComplete 74 | err = sess.Close() 75 | fatal(err, t) 76 | close(sessionClosed) 77 | }() 78 | 79 | addr := l.Addr().String() 80 | cfg := defaultTLSConfig.Clone() 81 | cfg.InsecureSkipVerify = true 82 | conn, err := quic.DialAddr(addr, cfg, nil) 83 | fatal(err, t) 84 | // defer conn.Close() 85 | 86 | sess := New(conn) 87 | 88 | var ch mux.Channel 89 | if !t.Run("session accept", func(t *testing.T) { 90 | ch, err = sess.Accept() 91 | fatal(err, t) 92 | }) { 93 | return 94 | } 95 | 96 | if !t.Run("channel write", func(t *testing.T) { 97 | _, err = ch.Write([]byte("Hello world")) 98 | fatal(err, t) 99 | err = ch.CloseWrite() 100 | fatal(err, t) 101 | }) { 102 | return 103 | } 104 | 105 | var b []byte 106 | if !t.Run("channel read", func(t *testing.T) { 107 | b, err = ioutil.ReadAll(ch) 108 | fatal(err, t) 109 | ch.Close() // should already be closed by other end 110 | }) { 111 | return 112 | } 113 | 114 | if !bytes.Equal(b, []byte("Hello world")) { 115 | t.Fatalf("unexpected bytes: %s", b) 116 | } 117 | close(testComplete) 118 | <-sessionClosed 119 | } 120 | 121 | func TestMultiChannelEcho(t *testing.T) { 122 | l, err := quic.ListenAddr("127.0.0.1:0", generateTLSConfig(), nil) 123 | fatal(err, t) 124 | defer l.Close() 125 | 126 | testComplete := make(chan struct{}) 127 | sessionClosed := make(chan struct{}) 128 | 129 | go func() { 130 | conn, err := l.Accept(context.Background()) 131 | fatal(err, t) 132 | // defer conn.Close() 133 | 134 | sess := New(conn) 135 | 136 | ch, err := sess.Open(context.Background()) 137 | fatal(err, t) 138 | b, err := ioutil.ReadAll(ch) 139 | fatal(err, t) 140 | ch.Close() // should already be closed by other end 141 | if string(b) != "Hello world" { 142 | t.Errorf("got: %#v", b) 143 | } 144 | 145 | ch, err = sess.Accept() 146 | fatal(err, t) 147 | _, err = ch.Write(b) 148 | fatal(err, t) 149 | err = ch.CloseWrite() 150 | fatal(err, t) 151 | 152 | <-testComplete 153 | err = sess.Close() 154 | fatal(err, t) 155 | close(sessionClosed) 156 | }() 157 | 158 | addr := l.Addr().String() 159 | cfg := defaultTLSConfig.Clone() 160 | cfg.InsecureSkipVerify = true 161 | conn, err := quic.DialAddr(addr, cfg, nil) 162 | fatal(err, t) 163 | // defer conn.Close() 164 | 165 | sess := New(conn) 166 | 167 | var ch mux.Channel 168 | if !t.Run("session accept", func(t *testing.T) { 169 | ch, err = sess.Accept() 170 | fatal(err, t) 171 | }) { 172 | return 173 | } 174 | 175 | if !t.Run("channel write", func(t *testing.T) { 176 | _, err = ch.Write([]byte("Hello world")) 177 | fatal(err, t) 178 | err = ch.Close() 179 | fatal(err, t) 180 | }) { 181 | return 182 | } 183 | 184 | if !t.Run("session open", func(t *testing.T) { 185 | ch, err = sess.Open(context.Background()) 186 | fatal(err, t) 187 | }) { 188 | return 189 | } 190 | 191 | var b []byte 192 | if !t.Run("channel read", func(t *testing.T) { 193 | b, err = ioutil.ReadAll(ch) 194 | fatal(err, t) 195 | ch.Close() // should already be closed by other end 196 | }) { 197 | return 198 | } 199 | 200 | if !bytes.Equal(b, []byte("Hello world")) { 201 | t.Fatalf("unexpected bytes: %s", b) 202 | } 203 | close(testComplete) 204 | <-sessionClosed 205 | } 206 | 207 | func TestOpenTimeout(t *testing.T) { 208 | t.Skipf("This test should detect that Open will time out if the remote side does not call Accept. However, that is not implemented yet.") 209 | 210 | l, err := quic.ListenAddr("127.0.0.1:0", generateTLSConfig(), nil) 211 | fatal(err, t) 212 | defer l.Close() 213 | 214 | testComplete := make(chan struct{}) 215 | sessionClosed := make(chan struct{}) 216 | 217 | go func() { 218 | conn, err := l.Accept(context.Background()) 219 | fatal(err, t) 220 | // defer conn.Close() 221 | 222 | sess := New(conn) 223 | 224 | <-testComplete 225 | err = sess.Close() 226 | fatal(err, t) 227 | close(sessionClosed) 228 | }() 229 | 230 | addr := l.Addr().String() 231 | cfg := defaultTLSConfig.Clone() 232 | cfg.InsecureSkipVerify = true 233 | conn, err := quic.DialAddr(addr, cfg, nil) 234 | fatal(err, t) 235 | // defer conn.Close() 236 | 237 | sess := New(conn) 238 | 239 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 240 | defer cancel() 241 | _, err = sess.Open(ctx) 242 | if err == nil { 243 | t.Fatalf("expected Open to time out") 244 | } 245 | 246 | close(testComplete) 247 | <-sessionClosed 248 | } 249 | -------------------------------------------------------------------------------- /exp/ptr.go: -------------------------------------------------------------------------------- 1 | package exp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/progrium/qtalk-go/fn" 13 | "github.com/progrium/qtalk-go/rpc" 14 | "github.com/rs/xid" 15 | ) 16 | 17 | // Ptr represents a remote function pointer. 18 | type Ptr struct { 19 | Ptr string `json:"$fnptr" mapstructure:"$fnptr"` 20 | Caller rpc.Caller `json:"-"` 21 | fn interface{} 22 | } 23 | 24 | // Call uses the Ptr Caller to call this remote function using the Ptr ID as the selector. 25 | // If Caller is not set on Ptr, Call will panic. Use SetCallers on incoming parameters that 26 | // may include Ptrs. 27 | func (p *Ptr) Call(ctx context.Context, params, reply interface{}) (*rpc.Response, error) { 28 | return p.Caller.Call(ctx, p.Ptr, params, reply) 29 | } 30 | 31 | // Callback wraps a function in a Ptr giving it a 20 character unique string ID. 32 | // A unique ID is created with every call, so should only be called once for a 33 | // given function. 34 | func Callback(fn interface{}) *Ptr { 35 | return &Ptr{ 36 | Ptr: xid.New().String(), 37 | fn: fn, 38 | } 39 | } 40 | 41 | // SetCallers will set the Caller for any Ptrs found in the value using PtrsFrom, as well as any 42 | // Ptrs encoded as maps, identifying them with the special key "$fnptr". Without Callers, Ptrs 43 | // will panic when using Call. This is often used on map[string]interface{} parameters before 44 | // running through something like github.com/mitchellh/mapstructure. 45 | func SetCallers(v interface{}, c rpc.Caller) []string { 46 | var ptrs []string 47 | for _, ptr := range PtrsFrom(v) { 48 | ptr.Caller = c 49 | ptrs = append(ptrs, ptr.Ptr) 50 | } 51 | walk(reflect.ValueOf(v), []string{}, func(v reflect.Value, parent reflect.Value, path []string) error { 52 | if path[len(path)-1] == "$fnptr" { 53 | parent.SetMapIndex(reflect.ValueOf("Caller"), reflect.ValueOf(c)) 54 | ptrs = append(ptrs, v.String()) 55 | } 56 | return nil 57 | }) 58 | return ptrs 59 | } 60 | 61 | // RegisterPtrs will register handlers on the RespondMux for Ptrs found using PtrsFrom on the value. 62 | // This is often called before making an RPC call that will include Ptr callbacks. It can safely be 63 | // called more than once for the same Ptrs, as it will only register handlers if they have not been 64 | // registered. They are registered on the RespondMux using the Ptr ID and a handler from HandlerFrom 65 | // on the Ptr function. 66 | func RegisterPtrs(m *rpc.RespondMux, v interface{}) { 67 | ptrs := PtrsFrom(v) 68 | for _, ptr := range ptrs { 69 | if h, _ := m.Match(ptr.Ptr); h == nil { 70 | m.Handle(ptr.Ptr, fn.HandlerFrom(ptr.fn)) 71 | } 72 | } 73 | } 74 | 75 | // UnregisterPtrs will remove handlers from the RespondMux matching Ptr IDs found using PtrsFrom on the value. 76 | func UnregisterPtrs(m *rpc.RespondMux, v interface{}) { 77 | ptrs := PtrsFrom(v) 78 | for _, ptr := range ptrs { 79 | if h, _ := m.Match(ptr.Ptr); h != nil { 80 | m.Remove(ptr.Ptr) 81 | } 82 | } 83 | } 84 | 85 | // PtrsFrom collects Ptrs from walking exported struct fields, slice/array elements, map values, and pointers in a value. 86 | func PtrsFrom(v interface{}) (ptrs []*Ptr) { 87 | typ := reflect.TypeOf(&Ptr{}) 88 | walk(reflect.ValueOf(v), []string{}, func(v reflect.Value, parent reflect.Value, path []string) error { 89 | if v.Type() == typ { 90 | vv := v.Interface().(*Ptr) 91 | if v.IsNil() { 92 | return nil 93 | } 94 | ptrs = append(ptrs, vv) 95 | } 96 | return nil 97 | }) 98 | return 99 | } 100 | 101 | func walk(v reflect.Value, path []string, visitor func(v reflect.Value, parent reflect.Value, path []string) error) error { 102 | for _, k := range keys(v) { 103 | subpath := append(path, k) 104 | vv := prop(v, k) 105 | if !vv.IsValid() { 106 | continue 107 | } 108 | if err := visitor(vv, v, subpath); err != nil { 109 | return err 110 | } 111 | if err := walk(vv, subpath, visitor); err != nil { 112 | return err 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | func prop(robj reflect.Value, key string) reflect.Value { 119 | rtyp := robj.Type() 120 | switch rtyp.Kind() { 121 | case reflect.Slice, reflect.Array: 122 | idx, err := strconv.Atoi(key) 123 | if err != nil { 124 | panic("non-numeric index given for slice") 125 | } 126 | rval := robj.Index(idx) 127 | if rval.IsValid() { 128 | return reflect.ValueOf(rval.Interface()) 129 | } 130 | case reflect.Ptr: 131 | return prop(robj.Elem(), key) 132 | case reflect.Map: 133 | rval := robj.MapIndex(reflect.ValueOf(key)) 134 | if rval.IsValid() { 135 | return reflect.ValueOf(rval.Interface()) 136 | } 137 | case reflect.Struct: 138 | rval := robj.FieldByName(key) 139 | if rval.IsValid() { 140 | return rval 141 | } 142 | for i := 0; i < rtyp.NumField(); i++ { 143 | field := rtyp.Field(i) 144 | tag := strings.Split(field.Tag.Get("json"), ",") 145 | if tag[0] == key || field.Name == key { 146 | return robj.FieldByName(field.Name) 147 | } 148 | } 149 | panic("struct field not found: " + key) 150 | } 151 | //spew.Dump(robj, key) 152 | panic("unexpected kind: " + rtyp.Kind().String()) 153 | } 154 | 155 | func keys(v reflect.Value) []string { 156 | switch v.Type().Kind() { 157 | case reflect.Map: 158 | var keys []string 159 | for _, key := range v.MapKeys() { 160 | k, ok := key.Interface().(string) 161 | if !ok { 162 | continue 163 | } 164 | keys = append(keys, k) 165 | } 166 | sort.Sort(sort.StringSlice(keys)) 167 | return keys 168 | case reflect.Struct: 169 | t := v.Type() 170 | var f []string 171 | for i := 0; i < t.NumField(); i++ { 172 | name := t.Field(i).Name 173 | // first letter capitalized means exported 174 | if name[0] == strings.ToUpper(name)[0] { 175 | f = append(f, name) 176 | } 177 | } 178 | return f 179 | case reflect.Slice, reflect.Array: 180 | var k []string 181 | for n := 0; n < v.Len(); n++ { 182 | k = append(k, strconv.Itoa(n)) 183 | } 184 | return k 185 | case reflect.Ptr: 186 | if !v.IsNil() { 187 | return keys(v.Elem()) 188 | } 189 | return []string{} 190 | case reflect.String, reflect.Bool, reflect.Float64, reflect.Float32, reflect.Interface: 191 | return []string{} 192 | default: 193 | fmt.Fprintf(os.Stderr, "unexpected type: %s\n", v.Type().Kind()) 194 | return []string{} 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /rpc/handler.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // A Handler responds to an RPC request. 11 | // 12 | // RespondRPC should use Call to receive at least one input argument value, then use 13 | // Responder to return a value or continue. Since an input argument value is always 14 | // sent to the handler, a call to Receive on the Call value shoud always be done otherwise 15 | // the call will block. You can call Receive with nil to discard the input value. If 16 | // Responder is not used, a default value of nil is returned. 17 | type Handler interface { 18 | RespondRPC(Responder, *Call) 19 | } 20 | 21 | // The HandlerFunc type is an adapter to allow the use of ordinary functions as RPC handlers. 22 | // If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f. 23 | type HandlerFunc func(Responder, *Call) 24 | 25 | // RespondRPC calls f(resp, call). 26 | func (f HandlerFunc) RespondRPC(resp Responder, call *Call) { 27 | f(resp, call) 28 | } 29 | 30 | // NotFoundHandler returns a simple handler that returns an error "not found". 31 | func NotFoundHandler() Handler { 32 | return HandlerFunc(func(r Responder, c *Call) { 33 | r.Return(fmt.Errorf("not found: %s", c.Selector)) 34 | }) 35 | } 36 | 37 | // RespondMux is an RPC call multiplexer. It matches the selector of each incoming call against a list of 38 | // registered selector patterns and calls the handler for the pattern that most closely matches the selector. 39 | // 40 | // RespondMux also takes care of normalizing the selector to a path form "/foo/bar", allowing you to use 41 | // this or the more conventional RPC dot form "foo.bar". 42 | // 43 | // Patterns match exact incoming selectors, or can end with a "/" or "." to indicate handling any selectors 44 | // beginning with this pattern. Longer patterns take precedence over shorter ones, so that if there are 45 | // handlers registered for both "foo." and "foo.bar.", the latter handler will be called for selectors 46 | // beginning "foo.bar." and the former will receive calls for any other selectors prefixed with "foo.". 47 | // 48 | // Since RespondMux is also a Handler, you can use them for submuxing. If a pattern matches a handler that 49 | // is a RespondMux, it will trim the matching selector prefix before matching against the sub RespondMux. 50 | type RespondMux struct { 51 | m map[string]muxEntry 52 | es []muxEntry // slice of entries sorted from longest to shortest. 53 | mu sync.RWMutex 54 | } 55 | 56 | type muxEntry struct { 57 | h Handler 58 | pattern string 59 | } 60 | 61 | type matcher interface { 62 | Match(selector string) (h Handler, pattern string) 63 | } 64 | 65 | // cleanSelector returns the canonical selector for s, normalizing . separators to /. 66 | func cleanSelector(s string) string { 67 | if s == "" { 68 | return "/" 69 | } 70 | if s[0] != '/' { 71 | s = "/" + s 72 | } 73 | s = strings.ReplaceAll(s, ".", "/") 74 | return s 75 | } 76 | 77 | // NewRespondMux allocates and returns a new RespondMux. 78 | func NewRespondMux() *RespondMux { return new(RespondMux) } 79 | 80 | // RespondRPC dispatches the call to the handler whose pattern most closely matches the selector. 81 | func (m *RespondMux) RespondRPC(r Responder, c *Call) { 82 | h, _ := m.Handler(c) 83 | h.RespondRPC(r, c) 84 | } 85 | 86 | // Handler returns the handler to use for the given call, consulting 87 | // c.Selector. It always returns a non-nil handler. 88 | // 89 | // If there is no registered handler that applies to the request, Handler 90 | // returns the FallbackHandler or if not set, a "not found" handler 91 | // with an empty pattern. 92 | func (m *RespondMux) Handler(c *Call) (h Handler, pattern string) { 93 | m.mu.RLock() 94 | defer m.mu.RUnlock() 95 | 96 | h, pattern = m.Match(c.Selector) 97 | if h == nil { 98 | h, pattern = NotFoundHandler(), "" 99 | } 100 | return 101 | } 102 | 103 | // Remove removes and returns the handler for the selector. 104 | func (m *RespondMux) Remove(selector string) (h Handler) { 105 | m.mu.Lock() 106 | defer m.mu.Unlock() 107 | 108 | selector = cleanSelector(selector) 109 | h = m.m[selector].h 110 | delete(m.m, selector) 111 | 112 | return 113 | } 114 | 115 | // Match finds a handler given a selector string. 116 | // Most-specific (longest) pattern wins. If a pattern handler 117 | // is a submux, it will call Match with the selector minus the 118 | // pattern. 119 | func (m *RespondMux) Match(selector string) (h Handler, pattern string) { 120 | selector = cleanSelector(selector) 121 | 122 | // Check for exact match first. 123 | v, ok := m.m[selector] 124 | if ok { 125 | return v.h, v.pattern 126 | } 127 | 128 | // Check for longest valid match. m.es contains all patterns 129 | // that end in / sorted from longest to shortest. 130 | for _, e := range m.es { 131 | if strings.HasPrefix(selector, e.pattern) { 132 | if m, ok := e.h.(matcher); ok { 133 | return m.Match(strings.TrimPrefix(selector, e.pattern)) 134 | } 135 | return e.h, e.pattern 136 | } 137 | } 138 | 139 | return nil, "" 140 | } 141 | 142 | // Handle registers the handler for the given pattern. 143 | // If a handler already exists for pattern, Handle panics. 144 | func (m *RespondMux) Handle(pattern string, handler Handler) { 145 | m.mu.Lock() 146 | defer m.mu.Unlock() 147 | 148 | pattern = cleanSelector(pattern) 149 | if _, ok := handler.(matcher); ok && pattern[len(pattern)-1] != '/' { 150 | pattern = pattern + "/" 151 | } 152 | 153 | if handler == nil { 154 | panic("rpc: nil handler") 155 | } 156 | if _, exist := m.m[pattern]; exist { 157 | panic("rpc: multiple registrations for " + pattern) 158 | } 159 | 160 | if m.m == nil { 161 | m.m = make(map[string]muxEntry) 162 | } 163 | e := muxEntry{h: handler, pattern: pattern} 164 | m.m[pattern] = e 165 | if pattern[len(pattern)-1] == '/' { 166 | m.es = appendSorted(m.es, e) 167 | } 168 | } 169 | 170 | func appendSorted(es []muxEntry, e muxEntry) []muxEntry { 171 | n := len(es) 172 | i := sort.Search(n, func(i int) bool { 173 | return len(es[i].pattern) < len(e.pattern) 174 | }) 175 | if i == n { 176 | return append(es, e) 177 | } 178 | // we now know that i points at where we want to insert 179 | es = append(es, muxEntry{}) // try to grow the slice in place, any entry works. 180 | copy(es[i+1:], es[i:]) // Move shorter entries down 181 | es[i] = e 182 | return es 183 | } 184 | -------------------------------------------------------------------------------- /mux/channel.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/progrium/qtalk-go/mux/frame" 10 | ) 11 | 12 | type channelDirection uint8 13 | 14 | const ( 15 | channelInbound channelDirection = iota 16 | channelOutbound 17 | ) 18 | 19 | func min(a uint32, b int) uint32 { 20 | if a < uint32(b) { 21 | return a 22 | } 23 | return uint32(b) 24 | } 25 | 26 | type Channel interface { 27 | io.ReadWriteCloser 28 | ID() uint32 29 | CloseWrite() error 30 | } 31 | 32 | // channel is an implementation of the Channel interface that works 33 | // with the session class. 34 | type channel struct { 35 | 36 | // R/O after creation 37 | localId, remoteId uint32 38 | 39 | // maxIncomingPayload and maxRemotePayload are the maximum 40 | // payload sizes of normal and extended data packets for 41 | // receiving and sending, respectively. The wire packet will 42 | // be 9 or 13 bytes larger (excluding encryption overhead). 43 | maxIncomingPayload uint32 44 | maxRemotePayload uint32 45 | 46 | session *session 47 | 48 | // direction contains either channelOutbound, for channels created 49 | // locally, or channelInbound, for channels created by the peer. 50 | direction channelDirection 51 | 52 | // Pending internal channel messages. 53 | msg chan frame.Message 54 | 55 | sentEOF bool 56 | 57 | // thread-safe data 58 | remoteWin window 59 | pending *buffer 60 | 61 | // windowMu protects myWindow, the flow-control window. 62 | windowMu sync.Mutex 63 | myWindow uint32 64 | 65 | // writeMu serializes calls to session.conn.Write() and 66 | // protects sentClose and packetPool. This mutex must be 67 | // different from windowMu, as writePacket can block if there 68 | // is a key exchange pending. 69 | writeMu sync.Mutex 70 | sentClose bool 71 | 72 | // packet buffer for writing 73 | packetBuf []byte 74 | } 75 | 76 | // ID returns the unique identifier of this channel 77 | // within the session 78 | func (ch *channel) ID() uint32 { 79 | return ch.localId 80 | } 81 | 82 | // CloseWrite signals the end of sending data. 83 | // The other side may still send data 84 | func (ch *channel) CloseWrite() error { 85 | ch.sentEOF = true 86 | return ch.send(frame.EOFMessage{ 87 | ChannelID: ch.remoteId}) 88 | } 89 | 90 | // Close signals end of channel use. No data may be sent after this 91 | // call. 92 | func (ch *channel) Close() error { 93 | return ch.send(frame.CloseMessage{ 94 | ChannelID: ch.remoteId}) 95 | } 96 | 97 | // Write writes len(data) bytes to the channel. 98 | func (ch *channel) Write(data []byte) (n int, err error) { 99 | if ch.sentEOF { 100 | return 0, io.EOF 101 | } 102 | 103 | for len(data) > 0 { 104 | space := min(ch.maxRemotePayload, len(data)) 105 | if space, err = ch.remoteWin.reserve(space); err != nil { 106 | return n, err 107 | } 108 | 109 | toSend := data[:space] 110 | 111 | if err = ch.session.enc.Encode(frame.DataMessage{ 112 | ChannelID: ch.remoteId, 113 | Length: uint32(len(toSend)), 114 | Data: toSend, 115 | }); err != nil { 116 | return n, err 117 | } 118 | 119 | n += len(toSend) 120 | data = data[len(toSend):] 121 | } 122 | 123 | return n, err 124 | } 125 | 126 | // Read reads up to len(data) bytes from the channel. 127 | func (c *channel) Read(data []byte) (n int, err error) { 128 | n, err = c.pending.Read(data) 129 | 130 | if n > 0 { 131 | err = c.adjustWindow(uint32(n)) 132 | // sendWindowAdjust can return io.EOF if the remote 133 | // peer has closed the connection, however we want to 134 | // defer forwarding io.EOF to the caller of Read until 135 | // the buffer has been drained. 136 | if n > 0 && err == io.EOF { 137 | err = nil 138 | } 139 | } 140 | return n, err 141 | } 142 | 143 | // sends writes a message frame. If the message is a channel close, it updates 144 | // sentClose. This method takes the lock c.writeMu. 145 | func (ch *channel) send(msg frame.Message) error { 146 | ch.writeMu.Lock() 147 | defer ch.writeMu.Unlock() 148 | 149 | if ch.sentClose { 150 | return io.EOF 151 | } 152 | 153 | if _, ok := msg.(frame.CloseMessage); ok { 154 | ch.sentClose = true 155 | } 156 | 157 | return ch.session.enc.Encode(msg) 158 | } 159 | 160 | func (c *channel) adjustWindow(n uint32) error { 161 | c.windowMu.Lock() 162 | // Since myWindow is managed on our side, and can never exceed 163 | // the initial window setting, we don't worry about overflow. 164 | c.myWindow += uint32(n) 165 | c.windowMu.Unlock() 166 | return c.send(frame.WindowAdjustMessage{ 167 | ChannelID: c.remoteId, 168 | AdditionalBytes: uint32(n), 169 | }) 170 | } 171 | 172 | func (c *channel) close() { 173 | c.pending.eof() 174 | close(c.msg) 175 | c.writeMu.Lock() 176 | // This is not necessary for a normal channel teardown, but if 177 | // there was another error, it is. 178 | c.sentClose = true 179 | c.writeMu.Unlock() 180 | // Unblock writers. 181 | c.remoteWin.close() 182 | } 183 | 184 | // responseMessageReceived is called when a success or failure message is 185 | // received on a channel to check that such a message is reasonable for the 186 | // given channel. 187 | func (ch *channel) responseMessageReceived() error { 188 | if ch.direction == channelInbound { 189 | return errors.New("qmux: channel response message received on inbound channel") 190 | } 191 | return nil 192 | } 193 | 194 | func (ch *channel) handle(msg frame.Message) error { 195 | switch m := msg.(type) { 196 | case *frame.DataMessage: 197 | return ch.handleData(m) 198 | 199 | case *frame.CloseMessage: 200 | ch.send(frame.CloseMessage{ 201 | ChannelID: ch.remoteId, 202 | }) 203 | ch.session.chans.remove(ch.localId) 204 | ch.close() 205 | return nil 206 | 207 | case *frame.EOFMessage: 208 | ch.pending.eof() 209 | return nil 210 | 211 | case *frame.WindowAdjustMessage: 212 | if !ch.remoteWin.add(m.AdditionalBytes) { 213 | return fmt.Errorf("qmux: invalid window update for %d bytes", m.AdditionalBytes) 214 | } 215 | return nil 216 | 217 | case *frame.OpenConfirmMessage: 218 | if err := ch.responseMessageReceived(); err != nil { 219 | return err 220 | } 221 | if m.MaxPacketSize < minPacketLength || m.MaxPacketSize > maxPacketLength { 222 | return fmt.Errorf("qmux: invalid MaxPacketSize %d from peer", m.MaxPacketSize) 223 | } 224 | ch.remoteId = m.SenderID 225 | ch.maxRemotePayload = m.MaxPacketSize 226 | ch.remoteWin.add(m.WindowSize) 227 | ch.msg <- m 228 | return nil 229 | 230 | case *frame.OpenFailureMessage: 231 | if err := ch.responseMessageReceived(); err != nil { 232 | return err 233 | } 234 | ch.session.chans.remove(m.ChannelID) 235 | ch.msg <- m 236 | return nil 237 | 238 | default: 239 | return fmt.Errorf("qmux: invalid channel message %v", msg) 240 | } 241 | } 242 | 243 | func (ch *channel) handleData(msg *frame.DataMessage) error { 244 | if msg.Length > ch.maxIncomingPayload { 245 | // TODO(hanwen): should send Disconnect? 246 | return errors.New("qmux: incoming packet exceeds maximum payload size") 247 | } 248 | 249 | if msg.Length != uint32(len(msg.Data)) { 250 | return errors.New("qmux: wrong packet length") 251 | } 252 | 253 | ch.windowMu.Lock() 254 | if ch.myWindow < msg.Length { 255 | ch.windowMu.Unlock() 256 | // TODO(hanwen): should send Disconnect with reason? 257 | return errors.New("qmux: remote side wrote too much") 258 | } 259 | ch.myWindow -= msg.Length 260 | ch.windowMu.Unlock() 261 | 262 | ch.pending.write(msg.Data) 263 | return nil 264 | } 265 | -------------------------------------------------------------------------------- /fn/handler_test.go: -------------------------------------------------------------------------------- 1 | package fn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/progrium/qtalk-go/codec" 11 | "github.com/progrium/qtalk-go/rpc" 12 | "github.com/progrium/qtalk-go/rpc/rpctest" 13 | ) 14 | 15 | func TestHandlerFromBadData(t *testing.T) { 16 | defer func() { 17 | if r := recover(); r == nil { 18 | t.Errorf("did not panic from bad argument data") 19 | } 20 | }() 21 | HandlerFrom(2) 22 | } 23 | 24 | type subfake struct { 25 | A string 26 | } 27 | 28 | type fake struct { 29 | A subfake 30 | B int 31 | } 32 | 33 | type id int 34 | 35 | func TestHandlerFromFunc(t *testing.T) { 36 | t.Run("int sum", func(t *testing.T) { 37 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) int { 38 | return a + b 39 | }), codec.JSONCodec{}) 40 | defer client.Close() 41 | 42 | var sum int 43 | if _, err := client.Call(context.Background(), "", []interface{}{2, 3}, &sum); err != nil { 44 | t.Fatal(err) 45 | } 46 | if sum != 5 { 47 | t.Fatalf("unexpected sum: %v", sum) 48 | } 49 | }) 50 | 51 | t.Run("defined type arg and return", func(t *testing.T) { 52 | client, _ := rpctest.NewPair(HandlerFrom(func(a id) id { 53 | return a 54 | }), codec.JSONCodec{}) 55 | defer client.Close() 56 | 57 | var ret id 58 | if _, err := client.Call(context.Background(), "", Args{id(64)}, &ret); err != nil { 59 | t.Fatal(err) 60 | } 61 | if ret != 64 { 62 | t.Fatalf("unexpected return value: %v", ret) 63 | } 64 | }) 65 | 66 | t.Run("struct arguments", func(t *testing.T) { 67 | client, _ := rpctest.NewPair(HandlerFrom(func(a fake, b subfake) { 68 | if a.A.A != "Hello" { 69 | t.Fatalf("unexpected field value in struct: %v", a) 70 | } 71 | if b.A != "world" { 72 | t.Fatalf("unexpected field value in struct: %v", b) 73 | } 74 | }), codec.JSONCodec{}) 75 | defer client.Close() 76 | 77 | if _, err := client.Call(context.Background(), "", Args{fake{A: subfake{A: "Hello"}}, subfake{A: "world"}}, nil); err != nil { 78 | t.Fatal(err) 79 | } 80 | }) 81 | 82 | t.Run("nil error", func(t *testing.T) { 83 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) error { 84 | return nil 85 | }), codec.JSONCodec{}) 86 | defer client.Close() 87 | 88 | if _, err := client.Call(context.Background(), "", []interface{}{2, 3}, nil); err != nil { 89 | t.Fatal(err) 90 | } 91 | }) 92 | 93 | t.Run("not enough args", func(t *testing.T) { 94 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) int { 95 | return a + b 96 | }), codec.JSONCodec{}) 97 | defer client.Close() 98 | 99 | var sum int 100 | _, err := client.Call(context.Background(), "", []interface{}{2}, &sum) 101 | if err == nil || !strings.Contains(err.Error(), "expected 2 params") { 102 | t.Fatalf("unexpected error: %v", err) 103 | } 104 | }) 105 | 106 | t.Run("too many args", func(t *testing.T) { 107 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) int { 108 | return a + b 109 | }), codec.JSONCodec{}) 110 | defer client.Close() 111 | 112 | var sum int 113 | _, err := client.Call(context.Background(), "", []interface{}{2, 3, 5}, &sum) 114 | if err == nil || !strings.Contains(err.Error(), "expected 2 params") { 115 | t.Fatalf("unexpected error: %v", err) 116 | } 117 | }) 118 | 119 | t.Run("with call", func(t *testing.T) { 120 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int, call *rpc.Call) int { 121 | if call.Selector != "/sum" { 122 | t.Fatalf("unexpected selector: %v", call.Selector) 123 | } 124 | return a + b 125 | }), codec.JSONCodec{}) 126 | defer client.Close() 127 | 128 | var sum int 129 | if _, err := client.Call(context.Background(), "sum", []interface{}{2, 3}, &sum); err != nil { 130 | t.Fatal(err) 131 | } 132 | if sum != 5 { 133 | t.Fatalf("unexpected sum: %v", sum) 134 | } 135 | }) 136 | 137 | t.Run("return error", func(t *testing.T) { 138 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) error { 139 | return errors.New("test") 140 | }), codec.JSONCodec{}) 141 | defer client.Close() 142 | 143 | var sum int 144 | _, err := client.Call(context.Background(), "", []interface{}{2, 3}, &sum) 145 | if err == nil || !strings.Contains(err.Error(), "test") { 146 | t.Fatalf("unexpected error: %v", err) 147 | } 148 | }) 149 | 150 | t.Run("return error with value", func(t *testing.T) { 151 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) (int, error) { 152 | return a + b, errors.New("test") 153 | }), codec.JSONCodec{}) 154 | defer client.Close() 155 | 156 | var sum int 157 | _, err := client.Call(context.Background(), "", []interface{}{2, 3}, &sum) 158 | if err == nil || !strings.Contains(err.Error(), "test") { 159 | t.Fatalf("unexpected error: %v", err) 160 | } 161 | }) 162 | 163 | t.Run("no return", func(t *testing.T) { 164 | client, _ := rpctest.NewPair(HandlerFrom(func(a, b int) { 165 | return 166 | }), codec.JSONCodec{}) 167 | defer client.Close() 168 | 169 | var sum int 170 | _, err := client.Call(context.Background(), "", []interface{}{2, 3}, &sum) 171 | if err != nil { 172 | t.Fatalf("unexpected error: %v", err) 173 | } 174 | }) 175 | 176 | } 177 | 178 | type mockMethods struct{} 179 | 180 | func (m *mockMethods) Foo() string { 181 | return "Foo" 182 | } 183 | 184 | func (m *mockMethods) Bar() {} 185 | 186 | func TestHandlerFromMethods(t *testing.T) { 187 | handler := HandlerFrom(&mockMethods{}) 188 | mux, ok := handler.(*rpc.RespondMux) 189 | if !ok { 190 | t.Fatal("expected handler to be rpc.RespondMux") 191 | } 192 | h, _ := mux.Match("Foo") 193 | if h == nil { 194 | t.Fatal("expected Foo handler") 195 | } 196 | h, _ = mux.Match("Bar") 197 | if h == nil { 198 | t.Fatal("expected Bar handler") 199 | } 200 | 201 | client, _ := rpctest.NewPair(mux, codec.JSONCodec{}) 202 | defer client.Close() 203 | 204 | var ret string 205 | if _, err := client.Call(context.Background(), "Foo", nil, &ret); err != nil { 206 | t.Fatal(err) 207 | } 208 | if ret != "Foo" { 209 | t.Fatalf("unexpected ret: %v", ret) 210 | } 211 | } 212 | 213 | func TestHandlerFromMethodsInterface(t *testing.T) { 214 | handler := HandlerFrom[interface { 215 | Foo() string 216 | }](&mockMethods{}) 217 | mux, ok := handler.(*rpc.RespondMux) 218 | if !ok { 219 | t.Fatal("expected handler to be rpc.RespondMux") 220 | } 221 | h, _ := mux.Match("Foo") 222 | if h == nil { 223 | t.Fatal("expected Foo handler") 224 | } 225 | h, _ = mux.Match("Bar") 226 | if h != nil { 227 | t.Fatal("expected no handler for Bar method not on interface") 228 | } 229 | 230 | client, _ := rpctest.NewPair(mux, codec.JSONCodec{}) 231 | defer client.Close() 232 | 233 | var ret string 234 | if _, err := client.Call(context.Background(), "Foo", nil, &ret); err != nil { 235 | t.Fatal(err) 236 | } 237 | if ret != "Foo" { 238 | t.Fatalf("unexpected ret: %v", ret) 239 | } 240 | } 241 | 242 | func TestHandlerFromMethodsInterfaceDifferentMethod(t *testing.T) { 243 | // Also check a different method to ensure that the reflection code is 244 | // matching the correct method based on the interface and not just getting 245 | // Method(0) which matches up in the first test. 246 | handler := HandlerFrom[interface { 247 | Bar() 248 | }](&mockMethods{}) 249 | mux, ok := handler.(*rpc.RespondMux) 250 | if !ok { 251 | t.Fatal("expected handler to be rpc.RespondMux") 252 | } 253 | h, _ := mux.Match("Bar") 254 | if h == nil { 255 | t.Fatal("expected Bar handler") 256 | } 257 | h, _ = mux.Match("Foo") 258 | if h != nil { 259 | t.Fatal("expected no handler for Foo method not on interface") 260 | } 261 | 262 | client, _ := rpctest.NewPair(mux, codec.JSONCodec{}) 263 | defer client.Close() 264 | 265 | var ret string 266 | if _, err := client.Call(context.Background(), "Bar", nil, &ret); err != nil { 267 | t.Fatal(err) 268 | } 269 | if ret != "" { 270 | t.Fatalf("unexpected ret: %v", ret) 271 | } 272 | } 273 | 274 | type handlerFuncMethod struct{} 275 | 276 | func (*handlerFuncMethod) Bar(r rpc.Responder, c *rpc.Call) { 277 | var args []any 278 | if err := c.Receive(&args); err != nil { 279 | r.Return(fmt.Errorf("fn: args: %s", err.Error())) 280 | return 281 | } 282 | 283 | r.Return("returned from Responder") 284 | } 285 | 286 | func TestMethodHandlerFunc(t *testing.T) { 287 | handler := HandlerFrom(&handlerFuncMethod{}) 288 | client, _ := rpctest.NewPair(handler, codec.JSONCodec{}) 289 | defer client.Close() 290 | 291 | var ret string 292 | if _, err := client.Call(context.Background(), "Bar", nil, &ret); err != nil { 293 | t.Fatal(err) 294 | } 295 | if ret != "returned from Responder" { 296 | t.Fatalf("unexpected ret: %v", ret) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /rpc/rpc_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/progrium/qtalk-go/codec" 13 | "github.com/progrium/qtalk-go/mux" 14 | ) 15 | 16 | func fatal(t *testing.T, err error) { 17 | t.Helper() 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | 23 | func newTestPair(handler Handler) (*Client, *Server) { 24 | ar, bw := io.Pipe() 25 | br, aw := io.Pipe() 26 | sessA, _ := mux.DialIO(aw, ar) 27 | sessB, _ := mux.DialIO(bw, br) 28 | 29 | srv := &Server{ 30 | Codec: codec.JSONCodec{}, 31 | Handler: handler, 32 | } 33 | go srv.Respond(sessA, nil) 34 | 35 | return NewClient(sessB, codec.JSONCodec{}), srv 36 | } 37 | 38 | func TestServerNoCodec(t *testing.T) { 39 | defer func() { 40 | if r := recover(); r == nil { 41 | t.Errorf("did not panic from unset codec") 42 | } 43 | }() 44 | 45 | ar, _ := io.Pipe() 46 | _, aw := io.Pipe() 47 | sessA, _ := mux.DialIO(aw, ar) 48 | 49 | srv := &Server{ 50 | Handler: NotFoundHandler(), 51 | } 52 | go sessA.Close() 53 | srv.Respond(sessA, nil) 54 | } 55 | 56 | func TestRespondMux(t *testing.T) { 57 | ctx := context.Background() 58 | 59 | t.Run("selector mux", func(t *testing.T) { 60 | mux := NewRespondMux() 61 | mux.Handle("foo", HandlerFunc(func(r Responder, c *Call) { 62 | r.Return("foo") 63 | })) 64 | mux.Handle("bar", HandlerFunc(func(r Responder, c *Call) { 65 | r.Return("bar") 66 | })) 67 | 68 | client, _ := newTestPair(mux) 69 | defer client.Close() 70 | 71 | var out string 72 | _, err := client.Call(ctx, "foo", nil, &out) 73 | fatal(t, err) 74 | if out != "foo" { 75 | t.Fatal("unexpected return:", out) 76 | } 77 | 78 | _, err = client.Call(ctx, "bar", nil, &out) 79 | fatal(t, err) 80 | if out != "bar" { 81 | t.Fatal("unexpected return:", out) 82 | } 83 | }) 84 | 85 | t.Run("selector not found error", func(t *testing.T) { 86 | mux := NewRespondMux() 87 | mux.Handle("foo", HandlerFunc(func(r Responder, c *Call) { 88 | r.Return("foo") 89 | })) 90 | 91 | client, _ := newTestPair(mux) 92 | defer client.Close() 93 | 94 | var out string 95 | _, err := client.Call(ctx, "baz", nil, &out) 96 | if err == nil { 97 | t.Fatal("expected error") 98 | } 99 | if err != nil { 100 | rErr, ok := err.(RemoteError) 101 | if !ok { 102 | t.Fatal("unexpected error:", err) 103 | } 104 | if rErr.Error() != "remote: not found: /baz" { 105 | t.Fatal("unexpected error:", rErr) 106 | } 107 | } 108 | }) 109 | 110 | t.Run("default handler mux", func(t *testing.T) { 111 | mux := NewRespondMux() 112 | mux.Handle("foo", HandlerFunc(func(r Responder, c *Call) { 113 | r.Return("foo") 114 | })) 115 | mux.Handle("", HandlerFunc(func(r Responder, c *Call) { 116 | r.Return(fmt.Errorf("default")) 117 | })) 118 | 119 | client, _ := newTestPair(mux) 120 | defer client.Close() 121 | 122 | var out string 123 | _, err := client.Call(ctx, "baz", nil, &out) 124 | if err == nil { 125 | t.Fatal("expected error") 126 | } 127 | if err != nil { 128 | rErr, ok := err.(RemoteError) 129 | if !ok { 130 | t.Fatal("unexpected error:", err) 131 | } 132 | if rErr.Error() != "remote: default" { 133 | t.Fatal("unexpected error:", rErr) 134 | } 135 | } 136 | 137 | _, err = client.Call(ctx, "foo", nil, &out) 138 | if err != nil { 139 | t.Fatal("unexpected error:", err) 140 | } 141 | if out != "foo" { 142 | t.Fatal("unexpected return:", out) 143 | } 144 | }) 145 | 146 | t.Run("sub muxing", func(t *testing.T) { 147 | mux := NewRespondMux() 148 | submux := NewRespondMux() 149 | mux.Handle("foo.bar", submux) 150 | mux.Handle("", HandlerFunc(func(r Responder, c *Call) { 151 | r.Return(fmt.Errorf("default")) 152 | })) 153 | submux.Handle("baz", HandlerFunc(func(r Responder, c *Call) { 154 | r.Return("foobarbaz") 155 | })) 156 | 157 | client, _ := newTestPair(mux) 158 | defer client.Close() 159 | 160 | var out string 161 | _, err := client.Call(ctx, "foo.bar.baz", nil, &out) 162 | fatal(t, err) 163 | if out != "foobarbaz" { 164 | t.Fatal("unexpected return:", out) 165 | } 166 | }) 167 | 168 | t.Run("selector normalizing", func(t *testing.T) { 169 | mux := NewRespondMux() 170 | mux.Handle("foo.bar", HandlerFunc(func(r Responder, c *Call) { 171 | r.Return("foobar") 172 | })) 173 | 174 | client, _ := newTestPair(mux) 175 | defer client.Close() 176 | 177 | var out string 178 | _, err := client.Call(ctx, "/foo/bar", nil, &out) 179 | fatal(t, err) 180 | if out != "foobar" { 181 | t.Fatal("unexpected return:", out) 182 | } 183 | }) 184 | 185 | t.Run("selector catchall", func(t *testing.T) { 186 | mux := NewRespondMux() 187 | mux.Handle("foo.bar.", HandlerFunc(func(r Responder, c *Call) { 188 | r.Return("foobar") 189 | })) 190 | 191 | client, _ := newTestPair(mux) 192 | defer client.Close() 193 | 194 | var out string 195 | _, err := client.Call(ctx, "foo.bar.baz", nil, &out) 196 | fatal(t, err) 197 | if out != "foobar" { 198 | t.Fatal("unexpected return:", out) 199 | } 200 | }) 201 | 202 | t.Run("remove handler", func(t *testing.T) { 203 | mux := NewRespondMux() 204 | mux.Handle("foo", HandlerFunc(func(r Responder, c *Call) { 205 | r.Return("foo") 206 | })) 207 | 208 | client, _ := newTestPair(mux) 209 | defer client.Close() 210 | 211 | _, err := client.Call(ctx, "foo", nil, nil) 212 | fatal(t, err) 213 | 214 | mux.Remove("foo") 215 | 216 | _, err = client.Call(ctx, "foo", nil, nil) 217 | if err == nil { 218 | t.Fatal("expected error") 219 | } 220 | }) 221 | 222 | t.Run("bad handler: nil", func(t *testing.T) { 223 | defer func() { 224 | if r := recover(); r == nil { 225 | t.Errorf("did not panic from nil handler") 226 | } 227 | }() 228 | mux := NewRespondMux() 229 | mux.Handle("foo.bar", nil) 230 | }) 231 | 232 | t.Run("bad handle: exists", func(t *testing.T) { 233 | defer func() { 234 | if r := recover(); r == nil { 235 | t.Errorf("did not panic from existing handle") 236 | } 237 | }() 238 | mux := NewRespondMux() 239 | mux.Handle("foo", NotFoundHandler()) 240 | mux.Handle("foo", NotFoundHandler()) 241 | }) 242 | } 243 | 244 | func TestRPC(t *testing.T) { 245 | ctx := context.Background() 246 | 247 | t.Run("unary rpc", func(t *testing.T) { 248 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 249 | var in string 250 | fatal(t, c.Receive(&in)) 251 | r.Return(in) 252 | })) 253 | defer client.Close() 254 | 255 | var out string 256 | resp, err := client.Call(ctx, "", "Hello world", &out) 257 | fatal(t, err) 258 | if resp.Continue { 259 | t.Fatal("unexpected continue") 260 | } 261 | if out != "Hello world" { 262 | t.Fatalf("unexpected return: %#v", out) 263 | } 264 | }) 265 | 266 | t.Run("unary rpc remote error", func(t *testing.T) { 267 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 268 | var in interface{} 269 | fatal(t, c.Receive(&in)) 270 | r.Return(fmt.Errorf("internal server error")) 271 | })) 272 | defer client.Close() 273 | 274 | var out string 275 | _, err := client.Call(ctx, "", "Hello world", &out) 276 | if err == nil { 277 | t.Fatal("expected error") 278 | } 279 | if err != nil { 280 | rErr, ok := err.(RemoteError) 281 | if !ok { 282 | t.Fatal("unexpected error:", err) 283 | } 284 | if rErr.Error() != "remote: internal server error" { 285 | t.Fatal("unexpected error:", rErr) 286 | } 287 | } 288 | }) 289 | 290 | t.Run("multi-return rpc", func(t *testing.T) { 291 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 292 | var in string 293 | fatal(t, c.Receive(&in)) 294 | r.Return(in, strings.ToUpper(in)) 295 | })) 296 | defer client.Close() 297 | 298 | var out, out2 string 299 | resp, err := client.Call(ctx, "", "Hello world", &out, &out2) 300 | fatal(t, err) 301 | if resp.Continue { 302 | t.Fatal("unexpected continue") 303 | } 304 | if out != "Hello world" { 305 | t.Errorf("unexpected return 1: %#v", out) 306 | } 307 | if out2 != "HELLO WORLD" { 308 | t.Errorf("unexpected return 2: %#v", out) 309 | } 310 | }) 311 | 312 | t.Run("server streaming rpc", func(t *testing.T) { 313 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 314 | var in string 315 | fatal(t, c.Receive(&in)) 316 | _, err := r.Continue(nil) 317 | fatal(t, err) 318 | fatal(t, r.Send(in)) 319 | fatal(t, r.Send(in)) 320 | fatal(t, r.Send(in)) 321 | })) 322 | defer client.Close() 323 | 324 | resp, err := client.Call(ctx, "", "Hello world", nil) 325 | fatal(t, err) 326 | if !resp.Continue { 327 | t.Fatal("expected continue") 328 | } 329 | for i := 0; i < 3; i++ { 330 | var rcv string 331 | fatal(t, resp.Receive(&rcv)) 332 | if rcv != "Hello world" { 333 | t.Fatalf("unexpected receive [%d]: %#v", i, rcv) 334 | } 335 | } 336 | 337 | }) 338 | 339 | t.Run("client streaming rpc", func(t *testing.T) { 340 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 341 | for i := 0; i < 3; i++ { 342 | var rcv string 343 | fatal(t, c.Receive(&rcv)) 344 | if rcv != "Hello world" { 345 | t.Fatalf("unexpected server receive [%d]: %#v", i, rcv) 346 | } 347 | } 348 | })) 349 | defer client.Close() 350 | 351 | sender := make(chan interface{}) 352 | go func() { 353 | for i := 0; i < 3; i++ { 354 | sender <- "Hello world" 355 | } 356 | close(sender) 357 | }() 358 | _, err := client.Call(ctx, "", sender, nil) 359 | fatal(t, err) 360 | 361 | }) 362 | 363 | t.Run("bidirectional streaming rpc", func(t *testing.T) { 364 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 365 | var rcv string 366 | for i := 0; i < 3; i++ { 367 | fatal(t, c.Receive(&rcv)) 368 | if rcv != "Hello world" { 369 | t.Fatalf("unexpected server receive [%d]: %#v", i, rcv) 370 | } 371 | } 372 | _, err := r.Continue(nil) 373 | fatal(t, err) 374 | fatal(t, r.Send(rcv)) 375 | fatal(t, r.Send(rcv)) 376 | fatal(t, r.Send(rcv)) 377 | })) 378 | defer client.Close() 379 | 380 | sender := make(chan interface{}) 381 | go func() { 382 | for i := 0; i < 3; i++ { 383 | sender <- "Hello world" 384 | } 385 | close(sender) 386 | }() 387 | resp, err := client.Call(ctx, "", sender, nil) 388 | fatal(t, err) 389 | if !resp.Continue { 390 | t.Fatal("expected continue") 391 | } 392 | for i := 0; i < 3; i++ { 393 | var rcv string 394 | fatal(t, resp.Receive(&rcv)) 395 | if rcv != "Hello world" { 396 | t.Fatalf("unexpected client receive [%d]: %#v", i, rcv) 397 | } 398 | } 399 | }) 400 | 401 | t.Run("bidirectional channel byte stream", func(t *testing.T) { 402 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 403 | fatal(t, c.Receive(nil)) 404 | ch, err := r.Continue(nil) 405 | fatal(t, err) 406 | io.Copy(ch, ch) 407 | ch.Close() 408 | })) 409 | defer client.Close() 410 | 411 | resp, err := client.Call(ctx, "", nil, nil) 412 | fatal(t, err) 413 | if !resp.Continue { 414 | t.Fatal("expected continue") 415 | } 416 | _, err = io.WriteString(resp.Channel, "Hello world") 417 | fatal(t, err) 418 | fatal(t, resp.Channel.CloseWrite()) 419 | b, err := ioutil.ReadAll(resp.Channel) 420 | fatal(t, err) 421 | if string(b) != "Hello world" { 422 | t.Fatalf("unexpected data: %#v", b) 423 | } 424 | }) 425 | 426 | t.Run("bidirectional channel codec stream", func(t *testing.T) { 427 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 428 | fatal(t, c.Receive(nil)) 429 | _, err := r.Continue(nil) 430 | fatal(t, err) 431 | 432 | var rcv string 433 | for i := 0; i < 3; i++ { 434 | fatal(t, c.Receive(&rcv)) 435 | if rcv != "Hello world" { 436 | t.Fatalf("unexpected server receive [%d]: %#v", i, rcv) 437 | } 438 | } 439 | fatal(t, r.Send(rcv)) 440 | fatal(t, r.Send(rcv)) 441 | fatal(t, r.Send(rcv)) 442 | })) 443 | defer client.Close() 444 | 445 | resp, err := client.Call(ctx, "", nil, nil) 446 | fatal(t, err) 447 | if !resp.Continue { 448 | t.Fatal("expected continue") 449 | } 450 | fatal(t, resp.Send("Hello world")) 451 | fatal(t, resp.Send("Hello world")) 452 | fatal(t, resp.Send("Hello world")) 453 | for i := 0; i < 3; i++ { 454 | var rcv string 455 | fatal(t, resp.Receive(&rcv)) 456 | if rcv != "Hello world" { 457 | t.Fatalf("unexpected client receive [%d]: %#v", i, rcv) 458 | } 459 | } 460 | }) 461 | 462 | t.Run("call timeout", func(t *testing.T) { 463 | client, _ := newTestPair(HandlerFunc(func(r Responder, c *Call) { 464 | time.Sleep(200 * time.Millisecond) 465 | fatal(t, c.Receive(nil)) 466 | _, err := r.Continue(nil) 467 | fatal(t, err) 468 | 469 | var rcv string 470 | for i := 0; i < 3; i++ { 471 | fatal(t, c.Receive(&rcv)) 472 | if rcv != "Hello world" { 473 | t.Fatalf("unexpected server receive [%d]: %#v", i, rcv) 474 | } 475 | } 476 | fatal(t, r.Send(rcv)) 477 | fatal(t, r.Send(rcv)) 478 | fatal(t, r.Send(rcv)) 479 | })) 480 | defer client.Close() 481 | 482 | ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) 483 | defer cancel() 484 | 485 | _, err := client.Call(ctx, "", []any{"foo"}, nil) 486 | expectedError := "context deadline exceeded" 487 | if fmt.Sprintf("%v", err) != expectedError { 488 | t.Fatalf("expected error: %v\ngot: %v", expectedError, err) 489 | } 490 | }) 491 | 492 | } 493 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 6 | github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= 7 | github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= 8 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 9 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 10 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 11 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 12 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 13 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 16 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 17 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 18 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 19 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 20 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 22 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 23 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 29 | github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= 30 | github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= 31 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 32 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 34 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 35 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 38 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 39 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 40 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 41 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 42 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 43 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 44 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 45 | github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= 46 | github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= 47 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 48 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 49 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 50 | github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= 51 | github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= 52 | github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= 53 | github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= 54 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= 55 | github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw= 56 | github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= 57 | github.com/pion/interceptor v0.1.18 h1:Hk26334NUQeUcJNR27YHYKT+sWNhhegQ9KFz5Nn6yMQ= 58 | github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I= 59 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 60 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 61 | github.com/pion/mdns v0.0.8 h1:HhicWIg7OX5PVilyBO6plhMetInbzkVJAhbdJiAeVaI= 62 | github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI= 63 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 64 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 65 | github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= 66 | github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= 67 | github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ= 68 | github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= 69 | github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= 70 | github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U= 71 | github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= 72 | github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= 73 | github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= 74 | github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4= 75 | github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc= 76 | github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= 77 | github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= 78 | github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= 79 | github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= 80 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= 81 | github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= 82 | github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA= 83 | github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= 84 | github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= 85 | github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= 86 | github.com/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY= 87 | github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/progrium/clon-go v0.0.0-20221124010328-fe21965c77cb h1:JOIT5Xis32KAzdeYPw4SAVi+XVmxu0/ZT23I8CGERV4= 91 | github.com/progrium/clon-go v0.0.0-20221124010328-fe21965c77cb/go.mod h1:mK13Psvk5yH/kw24KHy9ozYXrJAPtvyKqAdmqT7izfM= 92 | github.com/progrium/qtalk-go/x/cbor v0.0.0-20230306002123-cb3ad0c2cc62 h1:rXpaOdDKnTEQYdC0PpCifB95DSaoWGNI7vImVnj8urw= 93 | github.com/progrium/qtalk-go/x/cbor v0.0.0-20230306002123-cb3ad0c2cc62/go.mod h1:3Yu3P+h1zgkE3k2HAnSrYeXTXaU7/Hkyec/WSMoa058= 94 | github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= 95 | github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= 96 | github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= 97 | github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= 98 | github.com/quic-go/quic-go v0.33.1-0.20230330052113-c9ae15295683 h1:yq/zO2qMNHS458Jw/PuRM/8He2RCm2NauewymcB1TMs= 99 | github.com/quic-go/quic-go v0.33.1-0.20230330052113-c9ae15295683/go.mod h1:lyrnO7Hb6/5n1zcXfv1dvw8Az84BggTTrCKVE6a/SC0= 100 | github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 101 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 102 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 104 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 105 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 106 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 107 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 108 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 109 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 110 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 111 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 112 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 113 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 114 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 115 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 116 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 117 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 118 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 119 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 120 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 122 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 123 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 124 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 125 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 126 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 127 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= 128 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 129 | golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= 130 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 131 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 132 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 133 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 134 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 135 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 136 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 137 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 138 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 139 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 141 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 142 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 143 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 144 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 145 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 146 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 147 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 148 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 149 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 150 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 151 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 152 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 153 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 154 | golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= 155 | golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 156 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 157 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 158 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 184 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 190 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 192 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 193 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 194 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 195 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 196 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 197 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= 198 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 199 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 200 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 201 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 202 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 203 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 204 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 205 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 206 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 207 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 208 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 209 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 210 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 211 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 212 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 213 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 214 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 215 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 216 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 217 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 218 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 219 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 220 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 221 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 222 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 223 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 224 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 225 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 226 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 227 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 228 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 229 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 230 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 231 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 232 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 233 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 234 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 235 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 236 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 237 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 238 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 239 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 240 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 241 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 242 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 243 | --------------------------------------------------------------------------------