Repository: progrium/qmux Branch: main Commit: 475935a675d8 Files: 64 Total size: 85.7 KB Directory structure: gitextract_r4pm3g2i/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── README.md ├── SPEC.md ├── demos/ │ └── groktunnel/ │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── golang/ │ ├── README.md │ ├── codec/ │ │ ├── codec.go │ │ ├── codec_test.go │ │ ├── decoder.go │ │ ├── encoder.go │ │ ├── message.go │ │ ├── message_close.go │ │ ├── message_data.go │ │ ├── message_eof.go │ │ ├── message_open.go │ │ ├── message_openconfirm.go │ │ ├── message_openfailure.go │ │ └── message_windowadjust.go │ ├── go.mod │ ├── go.sum │ ├── mux/ │ │ ├── api.go │ │ ├── doc.go │ │ └── misc.go │ ├── session/ │ │ ├── channel.go │ │ ├── doc.go │ │ ├── session.go │ │ ├── session_test.go │ │ ├── util.go │ │ ├── util_buffer.go │ │ ├── util_chanlist.go │ │ └── util_window.go │ └── transport/ │ ├── dial_io.go │ ├── dial_net.go │ ├── dial_ws.go │ ├── doc.go │ ├── listen.go │ ├── listen_io.go │ ├── listen_net.go │ ├── listen_ws.go │ └── transport_test.go └── typescript/ ├── Makefile ├── README.md ├── api.ts ├── channel.ts ├── codec/ │ ├── codec_test.ts │ ├── decoder.ts │ ├── encoder.ts │ ├── index.ts │ └── message.ts ├── index.ts ├── internal.ts ├── session.ts ├── session_test.ts ├── transport/ │ ├── deno/ │ │ ├── tcp.ts │ │ └── websocket.ts │ └── websocket.ts ├── tsconfig.json └── util.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: progrium ================================================ FILE: .gitignore ================================================ TODO typescript/dist demos/groktunnel/groktunnel ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "denoland.vscode-deno", "golang.go" ] } ================================================ FILE: .vscode/settings.json ================================================ { "deno.enable": true, "deno.lint": true, "deno.unstable": true } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Jeff Lindsay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # qmux qmux is a wire protocol for multiplexing connections or streams into a single connection. It is based on the [SSH Connection Protocol](https://tools.ietf.org/html/rfc4254#page-5), which is the simplest, longest running, most widely deployed TCP multiplexing protocol with flow control. It is meant as a drop-in layer for any stream capable transport (TCP, WebSocket, stdio, etc) to provide basic multiplexing. This brings any connection API to rough semantic parity with [QUIC](https://en.wikipedia.org/wiki/QUIC) multiplexing, so it can act as a stopgap or fallback when QUIC is not available. You can then design higher level protocols based on multiplexing semantics that sit on top of QUIC or any other streaming transport with qmux. ## Spec The specification is [here](https://github.com/progrium/qmux/blob/main/SPEC.md). It is a simplified version of the [Channel Mechanism](https://tools.ietf.org/html/rfc4254#page-5) in the SSH Connection Protocol. ## Implementations - [x] [Golang](https://github.com/progrium/qmux/tree/main/golang) (best reference) - [x] [TypeScript](https://github.com/progrium/qmux/tree/main/typescript) - [ ] Python - [ ] C# (help wanted) ## Demos - [groktunnel](https://github.com/progrium/qmux/tree/main/demos/groktunnel): Ephemeral localhost public forwarding system for HTTP similar to Ngrok in less than 150 lines of Go. ## About Licensed MIT ================================================ FILE: SPEC.md ================================================ # qmux qmux is a wire protocol for multiplexing connections or streams into a single connection. It is a subset of the [SSH Connection Protocol](https://tools.ietf.org/html/rfc4254#page-5). Features removed to simplify include channel types, channel requests, and "extended data" messages that were used for STDERR data. ## Channels Either side may open a channel. Multiple channels are multiplexed into a single connection. Channels are identified by numbers at each end. The number referring to a channel may be different on each side. Requests to open a channel contain the sender's channel number. Any other channel- related messages contain the recipient's channel number for the channel. Channels are flow-controlled. No data may be sent to a channel until a message is received to indicate that window space is available. ### Opening a Channel When either side wishes to open a new channel, it allocates a local number for the channel. It then sends the following message to the other side, and includes the local channel number and initial window size in the message. byte QMUX_MSG_CHANNEL_OPEN uint32 sender channel uint32 initial window size uint32 maximum packet size The 'sender channel' is a local identifier for the channel used by the sender of this message. The 'initial window size' specifies how many bytes of channel data can be sent to the sender of this message without adjusting the window. The 'maximum packet size' specifies the maximum size of an individual data packet that can be sent to the sender. For example, one might want to use smaller packets for interactive connections to get better interactive response on slow links. The remote side then decides whether it can open the channel, and responds with either `QMUX_MSG_CHANNEL_OPEN_CONFIRMATION` or `QMUX_MSG_CHANNEL_OPEN_FAILURE`. byte QMUX_MSG_CHANNEL_OPEN_CONFIRMATION uint32 recipient channel uint32 sender channel uint32 initial window size uint32 maximum packet size The 'recipient channel' is the channel number given in the original open request, and 'sender channel' is the channel number allocated by the other side. byte QMUX_MSG_CHANNEL_OPEN_FAILURE uint32 recipient channel ### Data Transfer The window size specifies how many bytes the other party can send before it must wait for the window to be adjusted. Both parties use the following message to adjust the window. byte QMUX_MSG_CHANNEL_WINDOW_ADJUST uint32 recipient channel uint32 bytes to add After receiving this message, the recipient MAY send the given number of bytes more than it was previously allowed to send; the window size is incremented. Implementations MUST correctly handle window sizes of up to 2^32 - 1 bytes. The window MUST NOT be increased above 2^32 - 1 bytes. Data transfer is done with messages of the following type. byte QMUX_MSG_CHANNEL_DATA uint32 recipient channel string data The maximum amount of data allowed is determined by the maximum packet size for the channel, and the current window size, whichever is smaller. The window size is decremented by the amount of data sent. Both parties MAY ignore all extra data sent after the allowed window is empty. Implementations are expected to have some limit on the transport layer packet size. ### Closing a Channel When a party will no longer send more data to a channel, it SHOULD send `QMUX_MSG_CHANNEL_EOF`. byte QMUX_MSG_CHANNEL_EOF uint32 recipient channel No explicit response is sent to this message. However, the application may send EOF to whatever is at the other end of the channel. Note that the channel remains open after this message, and more data may still be sent in the other direction. This message does not consume window space and can be sent even if no window space is available. When either party wishes to terminate the channel, it sends `QMUX_MSG_CHANNEL_CLOSE`. Upon receiving this message, a party MUST send back an `QMUX_MSG_CHANNEL_CLOSE` unless it has already sent this message for the channel. The channel is considered closed for a party when it has both sent and received `QMUX_MSG_CHANNEL_CLOSE`, and the party may then reuse the channel number. A party MAY send `QMUX_MSG_CHANNEL_CLOSE` without having sent or received `QMUX_MSG_CHANNEL_EOF`. byte QMUX_MSG_CHANNEL_CLOSE uint32 recipient channel This message does not consume window space and can be sent even if no window space is available. It is RECOMMENDED that all data sent before this message be delivered to the actual destination, if possible. ## Summary of Message Numbers The following is a summary of messages and their associated message number byte value. QMUX_MSG_CHANNEL_OPEN 100 QMUX_MSG_CHANNEL_OPEN_CONFIRMATION 101 QMUX_MSG_CHANNEL_OPEN_FAILURE 102 QMUX_MSG_CHANNEL_WINDOW_ADJUST 103 QMUX_MSG_CHANNEL_DATA 104 QMUX_MSG_CHANNEL_EOF 105 QMUX_MSG_CHANNEL_CLOSE 106 ## Data Type Representations Used byte A byte represents an arbitrary 8-bit value (octet). Fixed length data is sometimes represented as an array of bytes, written byte[n], where n is the number of bytes in the array. uint32 Represents a 32-bit unsigned integer. Stored as four bytes in the order of decreasing significance (network byte order). For example: the value 699921578 (0x29b7f4aa) is stored as 29 b7 f4 aa. string Arbitrary length binary string. Strings are allowed to contain arbitrary binary data, including null characters and 8-bit characters. They are stored as a uint32 containing its length (number of bytes that follow) and zero (= empty string) or more bytes that are the value of the string. Terminating null characters are not used. ================================================ FILE: demos/groktunnel/README.md ================================================ # groktunnel Expose localhost HTTP servers with a public URL ## Build ``` $ go build ``` ## Try it out First we run the groktunnel server. Normally this would be run on a server, but by default uses `vcap.me` for a hostname which resolves all of its subdomains to localhost. ``` $ ./groktunnel 2021/04/29 16:10:35 groktunnel server [vcap.me] ready! ``` Now run a local web server. Here is how to run a server listing a file directory with Python: ``` $ python -m SimpleHTTPServer Serving HTTP on 0.0.0.0 port 8000 ... ``` Then we run groktunnel as a client by giving it the local port to expose. ``` $ ./groktunnel 8000 port 8000 http available at: http://y8eyshnpol.vcap.me:9999 ``` That address should serve the same content as the local web server on 8000. For added effect, run both client and server with `-p 80`, which will require root to run the server. ## About This uses qmux between the client and server to tunnel subdomain requests down to the client. This is done over a hijacked http connection after a tunnel is established and a new subdomain vhost is setup. Not counting dependencies, this whole system is done in under 150 lines. This is the 5th or 6th implementation of this system since the original [localtunnel](https://github.com/progrium/localtunnel) in 2010, which was then cloned many times. It was then commercialized by [Ngrok](https://ngrok.com/). ================================================ FILE: demos/groktunnel/go.mod ================================================ module github.com/progrium/qmux/demos/groktunnel go 1.16 require ( github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b github.com/progrium/qmux/golang v0.0.0-20210428195120-05a36e97c488 ) ================================================ FILE: demos/groktunnel/go.sum ================================================ github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE= github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= github.com/progrium/qmux/golang v0.0.0-20210428195120-05a36e97c488 h1:wG/GkKO0L/98gyqxx0Jm8CyVf/bPW3XUY8abCsFc5Yw= github.com/progrium/qmux/golang v0.0.0-20210428195120-05a36e97c488/go.mod h1:Z2EPtydgPrcZxO50GhkzTGgdWjA5PPHsZkoq6KTxPVE= golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ================================================ FILE: demos/groktunnel/main.go ================================================ package main import ( "bufio" "context" "crypto/rand" "flag" "fmt" "io" "log" "net" "net/http" "net/http/httputil" "strings" "time" vhost "github.com/inconshreveable/go-vhost" "github.com/progrium/qmux/golang/session" ) func main() { var port = flag.String("p", "9999", "server port to use") var host = flag.String("h", "vcap.me", "server hostname to use") var addr = flag.String("b", "127.0.0.1", "ip to bind [server only]") flag.Parse() // client usage: groktunnel [-h=] if flag.Arg(0) != "" { conn, err := net.Dial("tcp", net.JoinHostPort(*host, *port)) fatal(err) client := httputil.NewClientConn(conn, bufio.NewReader(conn)) req, err := http.NewRequest("GET", "/", nil) req.Host = net.JoinHostPort(*host, *port) fatal(err) client.Write(req) resp, _ := client.Read(req) fmt.Printf("port %s http available at:\n", flag.Arg(0)) fmt.Printf("http://%s\n", resp.Header.Get("X-Public-Host")) c, _ := client.Hijack() sess := session.New(c) defer sess.Close() for { ch, err := sess.Accept() fatal(err) conn, err := net.Dial("tcp", "127.0.0.1:"+flag.Arg(0)) fatal(err) go join(conn, ch) } return } // server usage: groktunnel [-h=] [-b=] l, err := net.Listen("tcp", net.JoinHostPort(*addr, *port)) fatal(err) defer l.Close() vmux, err := vhost.NewHTTPMuxer(l, 1*time.Second) fatal(err) go serve(vmux, *host, *port) log.Printf("groktunnel server [%s] ready!\n", *host) for { conn, err := vmux.NextError() fmt.Println(err) if conn != nil { conn.Close() } } } func serve(vmux *vhost.HTTPMuxer, host, port string) { ml, err := vmux.Listen(net.JoinHostPort(host, port)) fatal(err) srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { publicHost := strings.TrimSuffix(net.JoinHostPort(newSubdomain()+host, port), ":80") pl, err := vmux.Listen(publicHost) fatal(err) w.Header().Add("X-Public-Host", publicHost) w.Header().Add("Connection", "close") w.WriteHeader(http.StatusOK) conn, _, _ := w.(http.Hijacker).Hijack() sess := session.New(conn) defer sess.Close() log.Printf("%s: start session", publicHost) go func() { for { conn, err := pl.Accept() if err != nil { log.Println(err) return } ch, err := sess.Open(context.Background()) if err != nil { log.Println(err) return } go join(ch, conn) } }() sess.Wait() log.Printf("%s: end session", publicHost) })} srv.Serve(ml) } func join(a io.ReadWriteCloser, b io.ReadWriteCloser) { go io.Copy(b, a) io.Copy(a, b) a.Close() b.Close() } func newSubdomain() string { b := make([]byte, 10) if _, err := rand.Read(b); err != nil { panic(err) } letters := []rune("abcdefghijklmnopqrstuvwxyz1234567890") r := make([]rune, 10) for i := range r { r[i] = letters[int(b[i])*len(letters)/256] } return string(r) + "." } func fatal(err error) { if err != nil { log.Fatal(err) } } ================================================ FILE: golang/README.md ================================================ # qmux for Go An implementation of qmux for multiplexing any reliable `io.ReadWriteCloser`. ## Using qmux in Go ``` go get github.com/progrium/qmux/golang ``` You can create a qmux session from any `io.ReadWriteCloser` with `session.New`: ```go package main import ( "net" "io" "context" "github.com/progrium/qmux/golang/session" ) func main() { conn, err := net.Dial("tcp", "localhost:9999") if err != nil { panic(err) } sess := session.New(conn) defer sess.Close() // closes underlying conn ch, err := sess.Open(context.Background()) if err != nil { panic(err) } defer ch.Close() io.WriteString(ch, "Hello world\n") } ``` However it can be convenient to use the builtin transport dialers and listeners. ```go package main import ( "io/ioutil" "github.com/progrium/qmux/golang/transport" ) func main() { t, err := transport.ListenTCP("localhost:9999") if err != nil { panic(err) } defer t.Close() sess, err := t.Accept() if err != nil { panic(err) } defer sess.Close() ch, err := sess.Accept() if err != nil { panic(err) } defer ch.Close() b, err := ioutil.ReadAll(ch) if err != nil { panic(err) } os.Stdout.Write(b) // "Hello world\n" if connected with earlier program } ``` ================================================ FILE: golang/codec/codec.go ================================================ // Package codec implements encoding and decoding of qmux messages. package codec import "io" var ( DebugMessages io.Writer DebugBytes io.Writer ) ================================================ FILE: golang/codec/codec_test.go ================================================ package codec import ( "bytes" "testing" ) func TestMarshalUnmarshal(t *testing.T) { tests := []struct { in Message out Unmarshaler }{ { in: CloseMessage{ ChannelID: 10, }, out: &CloseMessage{}, }, { in: DataMessage{ ChannelID: 10, Length: 5, Data: []byte("Hello"), }, out: &DataMessage{}, }, { in: EOFMessage{ ChannelID: 10, }, out: &EOFMessage{}, }, { in: OpenMessage{ SenderID: 10, WindowSize: 1024, MaxPacketSize: 1 << 31, }, out: &OpenMessage{}, }, { in: OpenConfirmMessage{ ChannelID: 20, SenderID: 10, WindowSize: 1024, MaxPacketSize: 1 << 31, }, out: &OpenConfirmMessage{}, }, { in: OpenFailureMessage{ ChannelID: 20, }, out: &OpenFailureMessage{}, }, { in: WindowAdjustMessage{ ChannelID: 20, AdditionalBytes: 1024, }, out: &WindowAdjustMessage{}, }, } for _, test := range tests { b, err := Marshal(test.in) if err != nil { t.Fatal(err) } if err := Unmarshal(b, test.out); err != nil { t.Fatal(err) } bb, err := Marshal(test.out) if err != nil { t.Fatal(err) } if !bytes.Equal(b, bb) { t.Fatal("bytes not equal") } if test.in.String() != test.out.(Message).String() { t.Fatal("strings not equal") } } } func TestEncodeDecode(t *testing.T) { tests := []struct { in Message id uint32 ok bool }{ { in: CloseMessage{ ChannelID: 10, }, id: 10, ok: true, }, { in: DataMessage{ ChannelID: 10, Length: 5, Data: []byte("Hello"), }, id: 10, ok: true, }, { in: EOFMessage{ ChannelID: 10, }, id: 10, ok: true, }, { in: OpenMessage{ SenderID: 10, WindowSize: 1024, MaxPacketSize: 1 << 31, }, id: 0, ok: false, }, { in: OpenConfirmMessage{ ChannelID: 20, SenderID: 10, WindowSize: 1024, MaxPacketSize: 1 << 31, }, id: 20, ok: true, }, { in: OpenFailureMessage{ ChannelID: 20, }, id: 20, ok: true, }, { in: WindowAdjustMessage{ ChannelID: 20, AdditionalBytes: 1024, }, id: 20, ok: true, }, } for _, test := range tests { var buf bytes.Buffer enc := NewEncoder(&buf) if err := enc.Encode(test.in); err != nil { t.Fatal(err) } dec := NewDecoder(&buf) m, err := dec.Decode() if err != nil { t.Fatal(err) } id, ok := m.Channel() if id != test.id { t.Fatal("id not equal") } if ok != test.ok { t.Fatal("ok not equal") } } } ================================================ FILE: golang/codec/decoder.go ================================================ package codec import ( "encoding/binary" "errors" "fmt" "io" "os" "sync" "syscall" ) type Decoder struct { r io.Reader sync.Mutex } func NewDecoder(r io.Reader) *Decoder { return &Decoder{r: r} } func (dec *Decoder) Decode() (Message, error) { dec.Lock() defer dec.Unlock() packet, err := readPacket(dec.r) if err != nil { return nil, err } if DebugBytes != nil { fmt.Fprintln(DebugBytes, ">>DEC", packet) } return decode(packet) } func readPacket(c io.Reader) ([]byte, error) { msgNum := make([]byte, 1) _, err := c.Read(msgNum) if err != nil { var syscallErr *os.SyscallError if errors.As(err, &syscallErr) && syscallErr.Err == syscall.ECONNRESET { return nil, io.EOF } return nil, err } rest := make([]byte, payloadSizes[msgNum[0]]) _, err = c.Read(rest) if err != nil { return nil, err } packet := append(msgNum, rest...) if msgNum[0] == msgChannelData { dataSize := binary.BigEndian.Uint32(rest[4:8]) data := make([]byte, dataSize) _, err := c.Read(data) if err != nil { return nil, err } packet = append(packet, data...) } return packet, nil } func decode(packet []byte) (Message, error) { var msg Message switch packet[0] { case msgChannelOpen: msg = new(OpenMessage) case msgChannelData: msg = new(DataMessage) case msgChannelOpenConfirm: msg = new(OpenConfirmMessage) case msgChannelOpenFailure: msg = new(OpenFailureMessage) case msgChannelWindowAdjust: msg = new(WindowAdjustMessage) case msgChannelEOF: msg = new(EOFMessage) case msgChannelClose: msg = new(CloseMessage) default: return nil, fmt.Errorf("qmux: unexpected message type %d", packet[0]) } if err := Unmarshal(packet, msg); err != nil { return nil, err } if DebugMessages != nil { fmt.Fprintln(DebugMessages, ">>DEC", msg) } return msg, nil } type Unmarshaler interface { UnmarshalMux([]byte) error } func Unmarshal(b []byte, v interface{}) error { u, ok := v.(Unmarshaler) if !ok { return fmt.Errorf("qmux: unmarshal not supported for value %#v", v) } return u.UnmarshalMux(b) } ================================================ FILE: golang/codec/encoder.go ================================================ package codec import ( "fmt" "io" "sync" ) type Encoder struct { w io.Writer sync.Mutex } func NewEncoder(w io.Writer) *Encoder { return &Encoder{w: w} } func (enc *Encoder) Encode(msg interface{}) error { enc.Lock() defer enc.Unlock() if DebugMessages != nil { fmt.Fprintln(DebugMessages, "< 0 { space := min(ch.maxRemotePayload, len(data)) if space, err = ch.remoteWin.reserve(space); err != nil { return n, err } toSend := data[:space] if err = ch.session.enc.Encode(codec.DataMessage{ ChannelID: ch.remoteId, Length: uint32(len(toSend)), Data: toSend, }); err != nil { return n, err } n += len(toSend) data = data[len(toSend):] } return n, err } // Read reads up to len(data) bytes from the channel. func (c *Channel) Read(data []byte) (n int, err error) { n, err = c.pending.Read(data) if n > 0 { err = c.adjustWindow(uint32(n)) // sendWindowAdjust can return io.EOF if the remote // peer has closed the connection, however we want to // defer forwarding io.EOF to the caller of Read until // the buffer has been drained. if n > 0 && err == io.EOF { err = nil } } return n, err } // writePacket sends a packet. If the packet is a channel close, it updates // sentClose. This method takes the lock c.writeMu. func (ch *Channel) send(msg interface{}) error { ch.writeMu.Lock() defer ch.writeMu.Unlock() if ch.sentClose { return io.EOF } if _, ok := msg.(codec.CloseMessage); ok { ch.sentClose = true } return ch.session.enc.Encode(msg) } func (c *Channel) adjustWindow(n uint32) error { c.windowMu.Lock() // Since myWindow is managed on our side, and can never exceed // the initial window setting, we don't worry about overflow. c.myWindow += uint32(n) c.windowMu.Unlock() return c.send(codec.WindowAdjustMessage{ ChannelID: c.remoteId, AdditionalBytes: uint32(n), }) } func (c *Channel) close() { c.pending.eof() close(c.msg) c.writeMu.Lock() // This is not necessary for a normal channel teardown, but if // there was another error, it is. c.sentClose = true c.writeMu.Unlock() // Unblock writers. c.remoteWin.close() } // responseMessageReceived is called when a success or failure message is // received on a channel to check that such a message is reasonable for the // given channel. func (ch *Channel) responseMessageReceived() error { if ch.direction == channelInbound { return errors.New("qmux: channel response message received on inbound channel") } return nil } func (ch *Channel) handle(msg codec.Message) error { switch m := msg.(type) { case *codec.DataMessage: return ch.handleData(m) case *codec.CloseMessage: ch.send(codec.CloseMessage{ ChannelID: ch.remoteId, }) ch.session.chans.remove(ch.localId) ch.close() return nil case *codec.EOFMessage: ch.pending.eof() return nil case *codec.WindowAdjustMessage: if !ch.remoteWin.add(m.AdditionalBytes) { return fmt.Errorf("qmux: invalid window update for %d bytes", m.AdditionalBytes) } return nil case *codec.OpenConfirmMessage: if err := ch.responseMessageReceived(); err != nil { return err } if m.MaxPacketSize < minPacketLength || m.MaxPacketSize > maxPacketLength { return fmt.Errorf("qmux: invalid MaxPacketSize %d from peer", m.MaxPacketSize) } ch.remoteId = m.SenderID ch.maxRemotePayload = m.MaxPacketSize ch.remoteWin.add(m.WindowSize) ch.msg <- m return nil case *codec.OpenFailureMessage: if err := ch.responseMessageReceived(); err != nil { return err } ch.session.chans.remove(m.ChannelID) ch.msg <- m return nil default: return fmt.Errorf("qmux: invalid channel message %v", msg) } } func (ch *Channel) handleData(msg *codec.DataMessage) error { if msg.Length > ch.maxIncomingPayload { // TODO(hanwen): should send Disconnect? return errors.New("qmux: incoming packet exceeds maximum payload size") } if msg.Length != uint32(len(msg.Data)) { return errors.New("qmux: wrong packet length") } ch.windowMu.Lock() if ch.myWindow < msg.Length { ch.windowMu.Unlock() // TODO(hanwen): should send Disconnect with reason? return errors.New("qmux: remote side wrote too much") } ch.myWindow -= msg.Length ch.windowMu.Unlock() ch.pending.write(msg.Data) return nil } ================================================ FILE: golang/session/doc.go ================================================ // Package session implements a qmux session and channel API. package session ================================================ FILE: golang/session/session.go ================================================ package session import ( "context" "fmt" "io" "sync" "github.com/progrium/qmux/golang/codec" "github.com/progrium/qmux/golang/mux" ) const ( minPacketLength = 9 maxPacketLength = 1 << 31 // channelMaxPacket contains the maximum number of bytes that will be // sent in a single packet. As per RFC 4253, section 6.1, 32k is also // the minimum. channelMaxPacket = 1 << 15 // We follow OpenSSH here. channelWindowSize = 64 * channelMaxPacket // chanSize sets the amount of buffering qmux connections. This is // primarily for testing: setting chanSize=0 uncovers deadlocks more // quickly. chanSize = 16 ) // Session is a bi-directional channel muxing session on a given transport. type Session struct { t mux.Transport chans chanList enc *codec.Encoder dec *codec.Decoder inbox chan mux.Channel errCond *sync.Cond err error closeCh chan bool } // NewSession returns a session that runs over the given transport. func New(t mux.Transport) *Session { if t == nil { return nil } s := &Session{ t: t, enc: codec.NewEncoder(t), dec: codec.NewDecoder(t), inbox: make(chan mux.Channel, chanSize), errCond: sync.NewCond(new(sync.Mutex)), closeCh: make(chan bool, 1), } go s.loop() return s } // Close closes the underlying transport. func (s *Session) Close() error { s.t.Close() return nil } // Wait blocks until the transport has shut down, and returns the // error causing the shutdown. func (s *Session) Wait() error { s.errCond.L.Lock() defer s.errCond.L.Unlock() for s.err == nil { s.errCond.Wait() } return s.err } // Accept waits for and returns the next incoming channel. func (s *Session) Accept() (mux.Channel, error) { select { case ch := <-s.inbox: return ch, nil case <-s.closeCh: return nil, io.EOF } } // Open establishes a new channel with the other end. func (s *Session) Open(ctx context.Context) (mux.Channel, error) { ch := s.newChannel(channelOutbound) ch.maxIncomingPayload = channelMaxPacket if err := s.enc.Encode(codec.OpenMessage{ WindowSize: ch.myWindow, MaxPacketSize: ch.maxIncomingPayload, SenderID: ch.localId, }); err != nil { return nil, err } var m codec.Message select { case <-ctx.Done(): return nil, ctx.Err() case m = <-ch.msg: if m == nil { return nil, fmt.Errorf("qmux: channel closed early during open") } } switch msg := m.(type) { case *codec.OpenConfirmMessage: return ch, nil case *codec.OpenFailureMessage: return nil, fmt.Errorf("qmux: channel open failed on remote side") default: return nil, fmt.Errorf("qmux: unexpected packet in response to channel open: %v", msg) } } func (s *Session) newChannel(direction channelDirection) *Channel { ch := &Channel{ remoteWin: window{Cond: sync.NewCond(new(sync.Mutex))}, myWindow: channelWindowSize, pending: newBuffer(), direction: direction, msg: make(chan codec.Message, chanSize), session: s, packetBuf: make([]byte, 0), } ch.localId = s.chans.add(ch) return ch } // loop runs the connection machine. It will process packets until an // error is encountered. To synchronize on loop exit, use session.Wait. func (s *Session) loop() { var err error for err == nil { err = s.onePacket() } for _, ch := range s.chans.dropAll() { ch.close() } s.t.Close() s.closeCh <- true s.errCond.L.Lock() s.err = err s.errCond.Broadcast() s.errCond.L.Unlock() } // onePacket reads and processes one packet. func (s *Session) onePacket() error { var err error var msg codec.Message msg, err = s.dec.Decode() if err != nil { return err } id, isChan := msg.Channel() if !isChan { return s.handleOpen(msg.(*codec.OpenMessage)) } ch := s.chans.getChan(id) if ch == nil { return fmt.Errorf("qmux: invalid channel %d", id) } return ch.handle(msg) } // handleChannelOpen schedules a channel to be Accept()ed. func (s *Session) handleOpen(msg *codec.OpenMessage) error { if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > maxPacketLength { return s.enc.Encode(codec.OpenFailureMessage{ ChannelID: msg.SenderID, }) } c := s.newChannel(channelInbound) c.remoteId = msg.SenderID c.maxRemotePayload = msg.MaxPacketSize c.remoteWin.add(msg.WindowSize) c.maxIncomingPayload = channelMaxPacket s.inbox <- c return s.enc.Encode(codec.OpenConfirmMessage{ ChannelID: c.remoteId, SenderID: c.localId, WindowSize: c.myWindow, MaxPacketSize: c.maxIncomingPayload, }) } ================================================ FILE: golang/session/session_test.go ================================================ package session import ( "bytes" "context" "errors" "io/ioutil" "net" "testing" "time" "github.com/progrium/qmux/golang/mux" ) func fatal(err error, t *testing.T) { t.Helper() if err != nil { t.Fatal(err) } } func TestQmux(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") fatal(err, t) defer l.Close() go func() { conn, err := l.Accept() fatal(err, t) defer conn.Close() sess := New(conn) ch, err := sess.Open(context.Background()) fatal(err, t) b, err := ioutil.ReadAll(ch) fatal(err, t) ch.Close() // should already be closed by other end ch, err = sess.Accept() _, err = ch.Write(b) fatal(err, t) err = ch.CloseWrite() fatal(err, t) err = sess.Close() fatal(err, t) }() conn, err := net.Dial("tcp", l.Addr().String()) fatal(err, t) defer conn.Close() sess := New(conn) var ch mux.Channel t.Run("session accept", func(t *testing.T) { ch, err = sess.Accept() fatal(err, t) }) t.Run("channel write", func(t *testing.T) { _, err = ch.Write([]byte("Hello world")) fatal(err, t) err = ch.Close() fatal(err, t) }) t.Run("session open", func(t *testing.T) { ch, err = sess.Open(context.Background()) fatal(err, t) }) var b []byte t.Run("channel read", func(t *testing.T) { b, err = ioutil.ReadAll(ch) fatal(err, t) ch.Close() // should already be closed by other end }) if !bytes.Equal(b, []byte("Hello world")) { t.Fatalf("unexpected bytes: %s", b) } } func TestSessionOpenTimeout(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") fatal(err, t) defer l.Close() conn, err := net.Dial("tcp", l.Addr().String()) fatal(err, t) defer conn.Close() sess := New(conn) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() ch, err := sess.Open(ctx) if err != context.DeadlineExceeded { t.Fatalf("expected DeadlineExceeded, but got: %v", err) } if ch != nil { ch.Close() } } func TestSessionWait(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") fatal(err, t) defer l.Close() conn, err := net.Dial("tcp", l.Addr().String()) fatal(err, t) defer conn.Close() sess := New(conn) fatal(sess.Close(), t) // wait should return immediately since the connection was closed err = mux.Wait(sess) var netErr net.Error if !errors.As(err, &netErr) { t.Fatalf("expected a network error, but got: %v", err) } } ================================================ FILE: golang/session/util.go ================================================ package session func min(a uint32, b int) uint32 { if a < uint32(b) { return a } return uint32(b) } ================================================ FILE: golang/session/util_buffer.go ================================================ package session import ( "io" "sync" ) // buffer provides a linked list buffer for data exchange // between producer and consumer. Theoretically the buffer is // of unlimited capacity as it does no allocation of its own. type buffer struct { // protects concurrent access to head, tail and closed *sync.Cond head *element // the buffer that will be read first tail *element // the buffer that will be read last closed bool } // An element represents a single link in a linked list. type element struct { buf []byte next *element } // newBuffer returns an empty buffer that is not closed. func newBuffer() *buffer { e := new(element) b := &buffer{ Cond: sync.NewCond(new(sync.Mutex)), head: e, tail: e, } return b } // write makes buf available for Read to receive. // buf must not be modified after the call to write. func (b *buffer) write(buf []byte) { b.Cond.L.Lock() e := &element{buf: buf} b.tail.next = e b.tail = e b.Cond.Signal() b.Cond.L.Unlock() } // eof closes the buffer. Reads from the buffer once all // the data has been consumed will receive io.EOF. func (b *buffer) eof() { b.Cond.L.Lock() b.closed = true b.Cond.Signal() b.Cond.L.Unlock() } // Read reads data from the internal buffer in buf. Reads will block // if no data is available, or until the buffer is closed. func (b *buffer) Read(buf []byte) (n int, err error) { b.Cond.L.Lock() defer b.Cond.L.Unlock() for len(buf) > 0 { // if there is data in b.head, copy it if len(b.head.buf) > 0 { r := copy(buf, b.head.buf) buf, b.head.buf = buf[r:], b.head.buf[r:] n += r continue } // if there is a next buffer, make it the head if len(b.head.buf) == 0 && b.head != b.tail { b.head = b.head.next continue } // if at least one byte has been copied, return if n > 0 { break } // if nothing was read, and there is nothing outstanding // check to see if the buffer is closed. if b.closed { err = io.EOF break } // out of buffers, wait for producer b.Cond.Wait() } return } ================================================ FILE: golang/session/util_chanlist.go ================================================ package session import "sync" // chanList is a thread safe channel list. type chanList struct { // protects concurrent access to chans sync.Mutex // chans are indexed by the local id of the channel, which the // other side should send in the PeersId field. chans []*Channel } // Assigns a channel ID to the given channel. func (c *chanList) add(ch *Channel) uint32 { c.Lock() defer c.Unlock() for i := range c.chans { if c.chans[i] == nil { c.chans[i] = ch return uint32(i) } } c.chans = append(c.chans, ch) return uint32(len(c.chans) - 1) } // getChan returns the channel for the given ID. func (c *chanList) getChan(id uint32) *Channel { c.Lock() defer c.Unlock() if id < uint32(len(c.chans)) { return c.chans[id] } return nil } func (c *chanList) remove(id uint32) { c.Lock() if id < uint32(len(c.chans)) { c.chans[id] = nil } c.Unlock() } // dropAll forgets all channels it knows, returning them in a slice. func (c *chanList) dropAll() []*Channel { c.Lock() defer c.Unlock() var r []*Channel for _, ch := range c.chans { if ch == nil { continue } r = append(r, ch) } c.chans = nil return r } ================================================ FILE: golang/session/util_window.go ================================================ package session import ( "io" "sync" ) // window represents the buffer available to clients // wishing to write to a channel. type window struct { *sync.Cond win uint32 // RFC 4254 5.2 says the window size can grow to 2^32-1 writeWaiters int closed bool } // add adds win to the amount of window available // for consumers. func (w *window) add(win uint32) bool { // a zero sized window adjust is a noop. if win == 0 { return true } w.L.Lock() if w.win+win < win { w.L.Unlock() return false } w.win += win // It is unusual that multiple goroutines would be attempting to reserve // window space, but not guaranteed. Use broadcast to notify all waiters // that additional window is available. w.Broadcast() w.L.Unlock() return true } // close sets the window to closed, so all reservations fail // immediately. func (w *window) close() { w.L.Lock() w.closed = true w.Broadcast() w.L.Unlock() } // reserve reserves win from the available window capacity. // If no capacity remains, reserve will block. reserve may // return less than requested. func (w *window) reserve(win uint32) (uint32, error) { var err error w.L.Lock() w.writeWaiters++ w.Broadcast() for w.win == 0 && !w.closed { w.Wait() } w.writeWaiters-- if w.win < win { win = w.win } w.win -= win if w.closed { err = io.EOF } w.L.Unlock() return win, err } // waitWriterBlocked waits until some goroutine is blocked for further // writes. It is used in tests only. func (w *window) waitWriterBlocked() { w.Cond.L.Lock() for w.writeWaiters == 0 { w.Cond.Wait() } w.Cond.L.Unlock() } ================================================ FILE: golang/transport/dial_io.go ================================================ package transport import ( "io" "os" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" ) func DialIO(out io.WriteCloser, in io.ReadCloser) (mux.Session, error) { return session.New(&ioduplex{out, in}), nil } func DialStdio() (mux.Session, error) { return DialIO(os.Stdout, os.Stdin) } ================================================ FILE: golang/transport/dial_net.go ================================================ package transport import ( "net" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" ) func dialNet(proto, addr string) (mux.Session, error) { conn, err := net.Dial(proto, addr) if err != nil { return nil, err } return session.New(conn), nil } func DialTCP(addr string) (mux.Session, error) { return dialNet("tcp", addr) } func DialUnix(addr string) (mux.Session, error) { return dialNet("unix", addr) } ================================================ FILE: golang/transport/dial_ws.go ================================================ package transport import ( "fmt" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" "golang.org/x/net/websocket" ) func DialWS(addr string) (mux.Session, error) { ws, err := websocket.Dial(fmt.Sprintf("ws://%s/", addr), "", fmt.Sprintf("http://%s/", addr)) if err != nil { return nil, err } ws.PayloadType = websocket.BinaryFrame return session.New(ws), nil } ================================================ FILE: golang/transport/doc.go ================================================ // Package transport provides several dialers and listeners for getting qmux sessions over TCP, Unix sockets, WebSocket, and stdio. package transport ================================================ FILE: golang/transport/listen.go ================================================ package transport import "github.com/progrium/qmux/golang/mux" type Listener interface { // Close closes the listener. // Any blocked Accept operations will be unblocked and return errors. Close() error // Accept waits for and returns the next incoming session. Accept() (mux.Session, error) } ================================================ FILE: golang/transport/listen_io.go ================================================ package transport import ( "io" "os" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" ) type IOListener struct { io.ReadWriteCloser } func (l *IOListener) Accept() (mux.Session, error) { return session.New(l.ReadWriteCloser), nil } type ioduplex struct { io.WriteCloser io.ReadCloser } func (d *ioduplex) Close() error { if err := d.WriteCloser.Close(); err != nil { return err } if err := d.ReadCloser.Close(); err != nil { return err } return nil } func ListenIO(out io.WriteCloser, in io.ReadCloser) (*IOListener, error) { return &IOListener{ &ioduplex{out, in}, }, nil } func ListenStdio() (*IOListener, error) { return ListenIO(os.Stdout, os.Stdin) } ================================================ FILE: golang/transport/listen_net.go ================================================ package transport import ( "io" "net" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" ) type NetListener struct { net.Listener accepted chan mux.Session closer chan bool errs chan error } func (l *NetListener) Accept() (mux.Session, error) { select { case <-l.closer: return nil, io.EOF case err := <-l.errs: return nil, err case sess := <-l.accepted: return sess, nil } } // func (l *NetListener) Addr() net.Addr { // return l.Addr() // } func (l *NetListener) Close() error { if l.closer != nil { l.closer <- true } return l.Listener.Close() } func listenNet(proto, addr string) (*NetListener, error) { l, err := net.Listen(proto, addr) if err != nil { return nil, err } closer := make(chan bool, 1) errs := make(chan error, 1) accepted := make(chan mux.Session) go func(l net.Listener) { for { conn, err := l.Accept() if err != nil { errs <- err return } accepted <- session.New(conn) } }(l) return &NetListener{ Listener: l, errs: errs, accepted: accepted, closer: closer, }, nil } func ListenTCP(addr string) (*NetListener, error) { return listenNet("tcp", addr) } func ListenUnix(addr string) (*NetListener, error) { return listenNet("unix", addr) } ================================================ FILE: golang/transport/listen_ws.go ================================================ package transport import ( "net" "net/http" "github.com/progrium/qmux/golang/mux" "github.com/progrium/qmux/golang/session" "golang.org/x/net/websocket" ) func HandleWS(l *NetListener, ws *websocket.Conn) { ws.PayloadType = websocket.BinaryFrame sess := session.New(ws) defer sess.Close() l.accepted <- sess l.errs <- mux.Wait(sess) } func ListenWS(addr string) (*NetListener, error) { l, err := net.Listen("tcp", addr) if err != nil { return nil, err } nl := &NetListener{ Listener: l, accepted: make(chan mux.Session), errs: make(chan error, 2), closer: make(chan bool, 1), } s := &http.Server{ Addr: addr, Handler: websocket.Handler(func(ws *websocket.Conn) { HandleWS(nl, ws) }), } go func() { nl.errs <- s.Serve(l) }() return nl, nil } ================================================ FILE: golang/transport/transport_test.go ================================================ package transport import ( "bytes" "context" "io" "io/ioutil" "path" "testing" "github.com/progrium/qmux/golang/mux" ) func fatal(err error, t *testing.T) { t.Helper() if err != nil { t.Fatal(err) } } func testExchange(t *testing.T, sess mux.Session) { var err error var ch mux.Channel t.Run("session accept", func(t *testing.T) { ch, err = sess.Accept() fatal(err, t) }) t.Run("channel write", func(t *testing.T) { _, err = ch.Write([]byte("Hello world")) fatal(err, t) err = ch.Close() fatal(err, t) }) t.Run("session open", func(t *testing.T) { ch, err = sess.Open(context.Background()) fatal(err, t) }) var b []byte t.Run("channel read", func(t *testing.T) { b, err = ioutil.ReadAll(ch) fatal(err, t) err = ch.Close() fatal(err, t) }) if !bytes.Equal(b, []byte("Hello world")) { t.Fatalf("unexpected bytes: %s", b) } t.Run("session close", func(t *testing.T) { err = sess.Close() fatal(err, t) }) } func startListener(t *testing.T, l Listener) { t.Helper() t.Cleanup(func() { fatal(l.Close(), t) }) go func() { sess, err := l.Accept() fatal(err, t) t.Cleanup(func() { // Synchronizes cleanup, waiting for the client to disconnect before // closing the stream. This prevents errors in the Pipe-based test with // closing one end of the pipe before the other has read the data. // Registering as a test cleanup function also avoids a race condition // with the test exiting before closing the session. if err := mux.Wait(sess); err != io.EOF { t.Errorf("Wait returned unexpected error: %v", err) } err = sess.Close() fatal(err, t) }) ch, err := sess.Open(context.Background()) fatal(err, t) b, err := ioutil.ReadAll(ch) fatal(err, t) ch.Close() ch, err = sess.Accept() _, err = ch.Write(b) fatal(err, t) err = ch.CloseWrite() fatal(err, t) }() } func TestTCP(t *testing.T) { l, err := ListenTCP("127.0.0.1:0") fatal(err, t) startListener(t, l) sess, err := DialTCP(l.Addr().String()) fatal(err, t) testExchange(t, sess) } func TestUnix(t *testing.T) { tmp := t.TempDir() sockPath := path.Join(tmp, "qmux.sock") l, err := ListenUnix(sockPath) fatal(err, t) startListener(t, l) sess, err := DialUnix(sockPath) fatal(err, t) testExchange(t, sess) } func TestIO(t *testing.T) { pr1, pw1 := io.Pipe() pr2, pw2 := io.Pipe() l, err := ListenIO(pw1, pr2) fatal(err, t) startListener(t, l) sess, err := DialIO(pw2, pr1) fatal(err, t) testExchange(t, sess) } func TestWS(t *testing.T) { l, err := ListenWS("127.0.0.1:0") fatal(err, t) startListener(t, l) sess, err := DialWS(l.Addr().String()) fatal(err, t) testExchange(t, sess) } ================================================ FILE: typescript/Makefile ================================================ build: dist/qmux.js dist/qmux.js: **.ts mkdir -p dist deno bundle -c tsconfig.json index.ts dist/qmux.js ================================================ FILE: typescript/README.md ================================================ # qmux for TypeScript ================================================ FILE: typescript/api.ts ================================================ export interface IConn { read(len: number): Promise; write(buffer: Uint8Array): Promise; close(): Promise; } export interface ISession { open(): Promise; accept(): Promise; close(): Promise; } export interface IChannel extends IConn { ident(): number closeWrite(): Promise } export interface IConnListener { accept(): Promise; close(): Promise; } ================================================ FILE: typescript/channel.ts ================================================ // @ts-ignore import * as util from "./util.ts"; // @ts-ignore import * as codec from "./codec/index.ts"; // @ts-ignore import * as internal from "./internal.ts"; export const channelMaxPacket = 1 << 15; export const channelWindowSize = 64 * channelMaxPacket; // channel represents a virtual muxed connection export class Channel { localId: number; remoteId: number; maxIncomingPayload: number; maxRemotePayload: number; session: internal.Session; ready: util.queue; sentEOF: boolean; sentClose: boolean; remoteWin: number; myWindow: number; readBuf: util.ReadBuffer; writers: Array<() => void>; constructor(sess: internal.Session) { this.localId = 0; this.remoteId = 0; this.maxIncomingPayload = 0; this.maxRemotePayload = 0; this.sentEOF = false; this.sentClose = false; this.remoteWin = 0; this.myWindow = 0; this.ready = new util.queue(); this.session = sess; this.writers = []; this.readBuf = new util.ReadBuffer(); } ident(): number { return this.localId; } async read(len: number): Promise { let data = await this.readBuf.read(len); if (data !== undefined) { try { await this.adjustWindow(data.byteLength) } catch (e) { if (e !== "EOF") { throw e; } } } return data; } reserveWindow(win: number): number { if (this.remoteWin < win) { win = this.remoteWin; } this.remoteWin -= win; return win; } addWindow(win: number) { this.remoteWin += win; while (this.remoteWin > 0) { let writer = this.writers.shift(); if (!writer) break; writer(); } } write(buffer: Uint8Array): Promise { if (this.sentEOF) { return Promise.reject("EOF"); } return new Promise((resolve, reject) => { let n = 0; let tryWrite = () => { if (this.sentEOF || this.sentClose) { reject("EOF"); return; } if (buffer.byteLength == 0) { resolve(n); return; } let space = Math.min(this.maxRemotePayload, buffer.byteLength); let reserved = this.reserveWindow(space); if (reserved == 0) { this.writers.push(tryWrite); return; } let toSend = buffer.slice(0, reserved); this.send({ ID: codec.DataID, channelID: this.remoteId, length: toSend.byteLength, data: toSend, }).then(() => { n += toSend.byteLength; buffer = buffer.slice(toSend.byteLength); if (buffer.byteLength == 0) { resolve(n); return; } this.writers.push(tryWrite); }) } tryWrite(); }) } async closeWrite() { this.sentEOF = true; await this.send({ ID: codec.EofID, channelID: this.remoteId }); this.writers.forEach(writer => writer()); this.writers = []; } async close(): Promise { if (!this.sentClose) { await this.send({ ID: codec.CloseID, channelID: this.remoteId }); this.sentClose = true; while (await this.ready.shift() !== undefined) { } return; } this.shutdown(); } shutdown(): void { this.readBuf.close(); this.writers.forEach(writer => writer()); this.ready.close(); this.session.rmCh(this.localId); } async adjustWindow(n: number) { // Since myWindow is managed on our side, and can never exceed // the initial window setting, we don't worry about overflow. this.myWindow += n; await this.send({ ID: codec.WindowAdjustID, channelID: this.remoteId, additionalBytes: n, }) } send(msg: codec.ChannelMessage): Promise { if (this.sentClose) { throw "EOF"; } this.sentClose = (msg.ID === codec.CloseID); return this.session.enc.encode(msg); } handle(msg: codec.ChannelMessage): void { if (msg.ID === codec.DataID) { this.handleData(msg as codec.DataMessage); return; } if (msg.ID === codec.CloseID) { this.close(); // is this right? return; } if (msg.ID === codec.EofID) { this.readBuf.eof(); } if (msg.ID === codec.OpenFailureID) { this.session.rmCh(msg.channelID); this.ready.push(false); return; } if (msg.ID === codec.OpenConfirmID) { if (msg.maxPacketSize < internal.minPacketLength || msg.maxPacketSize > internal.maxPacketLength) { throw "invalid max packet size"; } this.remoteId = msg.senderID; this.maxRemotePayload = msg.maxPacketSize; this.addWindow(msg.windowSize); this.ready.push(true); return; } if (msg.ID === codec.WindowAdjustID) { this.addWindow(msg.additionalBytes); } } handleData(msg: codec.DataMessage) { if (msg.length > this.maxIncomingPayload) { throw "incoming packet exceeds maximum payload size"; } // TODO: check packet length if (this.myWindow < msg.length) { throw "remote side wrote too much"; } this.myWindow -= msg.length; this.readBuf.write(msg.data) } } ================================================ FILE: typescript/codec/codec_test.ts ================================================ import { assertEquals, } from "https://deno.land/std/testing/asserts.ts"; // @ts-ignore import * as codec from "./index.ts"; import * as msg from "./message.ts"; Deno.test("hello world #1", () => { let packet = new Uint8Array(5); packet.set([105, 0, 0, 0, 0]); let obj = codec.Unmarshal(packet) as msg.AnyMessage; let buf = codec.Marshal(obj); console.log("Hello", obj, buf); }); ================================================ FILE: typescript/codec/decoder.ts ================================================ // @ts-ignore import * as msg from "./message.ts"; // @ts-ignore import * as api from "../api.ts"; // @ts-ignore import * as util from "../util.ts"; export class Decoder { conn: api.IConn; debug: boolean; constructor(conn: api.IConn, debug: boolean = false) { this.conn = conn; this.debug = debug; } async decode(): Promise { let packet = await readPacket(this.conn); if (packet === undefined) { return Promise.resolve(undefined); } let msg = Unmarshal(packet); if (this.debug) { console.log(">>", msg); } return msg; } } async function readPacket(conn: api.IConn): Promise { let head = await conn.read(1); if (head === undefined) { return Promise.resolve(undefined); } let msgID = head[0]; let size = msg.payloadSizes.get(msgID); if (size === undefined || msgID < msg.OpenID || msgID > msg.CloseID) { return Promise.reject(`bad packet: ${msgID}`); } let rest = await conn.read(size); if (rest === undefined) { return Promise.reject("unexpected EOF"); } if (msgID === msg.DataID) { let view = new DataView(rest.buffer); let length = view.getUint32(4); let data = await conn.read(length); if (data === undefined) { return Promise.reject("unexpected EOF"); } return util.concat([head, rest, data], length + rest.length + 1); } return util.concat([head, rest], rest.length + 1); } export function Unmarshal(packet: Uint8Array): msg.Message { let data = new DataView(packet.buffer); switch (packet[0]) { case msg.CloseID: return { ID: packet[0], channelID: data.getUint32(1) } as msg.CloseMessage; case msg.DataID: let dataLength = data.getUint32(5); let rest = new Uint8Array(packet.buffer.slice(9)); return { ID: packet[0], channelID: data.getUint32(1), length: dataLength, data: rest, } as msg.DataMessage; case msg.EofID: return { ID: packet[0], channelID: data.getUint32(1) } as msg.EOFMessage; case msg.OpenID: return { ID: packet[0], senderID: data.getUint32(1), windowSize: data.getUint32(5), maxPacketSize: data.getUint32(9), } as msg.OpenMessage; case msg.OpenConfirmID: return { ID: packet[0], channelID: data.getUint32(1), senderID: data.getUint32(5), windowSize: data.getUint32(9), maxPacketSize: data.getUint32(13), } as msg.OpenConfirmMessage; case msg.OpenFailureID: return { ID: packet[0], channelID: data.getUint32(1), } as msg.OpenFailureMessage; case msg.WindowAdjustID: return { ID: packet[0], channelID: data.getUint32(1), additionalBytes: data.getUint32(5), } as msg.WindowAdjustMessage; default: throw `unmarshal of unknown type: ${packet[0]}`; } } ================================================ FILE: typescript/codec/encoder.ts ================================================ // @ts-ignore import * as api from "../api.ts"; // @ts-ignore import * as msg from "./message.ts"; export class Encoder { conn: api.IConn; debug: boolean; constructor(conn: api.IConn, debug: boolean = false) { this.conn = conn; this.debug = debug; } async encode(m: msg.AnyMessage): Promise { if (this.debug) { console.log("<<", m); } return this.conn.write(Marshal(m)); } } export function Marshal(obj: msg.AnyMessage): Uint8Array { if (obj.ID === msg.CloseID) { let m = obj as msg.CloseMessage; let data = new DataView(new ArrayBuffer(5)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); return new Uint8Array(data.buffer); } if (obj.ID === msg.DataID) { let m = obj as msg.DataMessage; let data = new DataView(new ArrayBuffer(9)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); data.setUint32(5, m.length); let buf = new Uint8Array(9 + m.length); buf.set(new Uint8Array(data.buffer), 0); buf.set(m.data, 9); return buf; } if (obj.ID === msg.EofID) { let m = obj as msg.EOFMessage; let data = new DataView(new ArrayBuffer(5)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); return new Uint8Array(data.buffer); } if (obj.ID === msg.OpenID) { let m = obj as msg.OpenMessage; let data = new DataView(new ArrayBuffer(13)); data.setUint8(0, m.ID); data.setUint32(1, m.senderID); data.setUint32(5, m.windowSize); data.setUint32(9, m.maxPacketSize); return new Uint8Array(data.buffer); } if (obj.ID === msg.OpenConfirmID) { let m = obj as msg.OpenConfirmMessage; let data = new DataView(new ArrayBuffer(17)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); data.setUint32(5, m.senderID); data.setUint32(9, m.windowSize); data.setUint32(13, m.maxPacketSize); return new Uint8Array(data.buffer); } if (obj.ID === msg.OpenFailureID) { let m = obj as msg.OpenFailureMessage; let data = new DataView(new ArrayBuffer(5)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); return new Uint8Array(data.buffer); } if (obj.ID === msg.WindowAdjustID) { let m = obj as msg.WindowAdjustMessage; let data = new DataView(new ArrayBuffer(9)); data.setUint8(0, m.ID); data.setUint32(1, m.channelID); data.setUint32(5, m.additionalBytes); return new Uint8Array(data.buffer); } throw `marshal of unknown type: ${obj}`; } ================================================ FILE: typescript/codec/index.ts ================================================ // @ts-ignore export * from "./message.ts"; // @ts-ignore export * from "./encoder.ts"; // @ts-ignore export * from "./decoder.ts"; ================================================ FILE: typescript/codec/message.ts ================================================ export const OpenID = 100; export const OpenConfirmID = 101; export const OpenFailureID = 102; export const WindowAdjustID = 103; export const DataID = 104; export const EofID = 105; export const CloseID = 106; export var payloadSizes = new Map([ [OpenID, 12], [OpenConfirmID, 16], [OpenFailureID, 4], [WindowAdjustID, 8], [DataID, 8], [EofID, 4], [CloseID, 4], ]); export interface Message { ID: number; } export interface OpenMessage { ID: 100; senderID: number; windowSize: number; maxPacketSize: number; } export interface OpenConfirmMessage { ID: 101; channelID: number; senderID: number; windowSize: number; maxPacketSize: number; } export interface OpenFailureMessage { ID: 102; channelID: number; } export interface WindowAdjustMessage { ID: 103; channelID: number; additionalBytes: number; } export interface DataMessage { ID: 104; channelID: number; length: number; data: Uint8Array; } export interface EOFMessage { ID: 105; channelID: number; } export interface CloseMessage { ID: 106; channelID: number; } export type ChannelMessage = ( OpenConfirmMessage | OpenFailureMessage | WindowAdjustMessage | DataMessage | EOFMessage | CloseMessage); export type AnyMessage = ChannelMessage | OpenMessage; ================================================ FILE: typescript/index.ts ================================================ // https://github.com/Microsoft/TypeScript/issues/27481 // @ts-ignore export * from "./internal.ts"; // @ts-ignore export * from "./transport/websocket.ts"; ================================================ FILE: typescript/internal.ts ================================================ // @ts-ignore export * from "./session.ts"; // @ts-ignore export * from "./channel.ts"; ================================================ FILE: typescript/session.ts ================================================ // @ts-ignore import * as api from "./api.ts"; // @ts-ignore import * as codec from "./codec/index.ts"; // @ts-ignore import * as util from "./util.ts"; // @ts-ignore import * as internal from "./internal.ts"; export const minPacketLength = 9; export const maxPacketLength = Number.MAX_VALUE; export class Session implements api.ISession { conn: api.IConn; channels: Array; incoming: util.queue; enc: codec.Encoder; dec: codec.Decoder; done: Promise; constructor(conn: api.IConn, debug: boolean = false) { this.conn = conn; this.enc = new codec.Encoder(conn, debug); this.dec = new codec.Decoder(conn, debug); this.channels = []; this.incoming = new util.queue(); this.done = this.loop(); } async open(): Promise { let ch = this.newChannel(); ch.maxIncomingPayload = internal.channelMaxPacket; await this.enc.encode({ ID: codec.OpenID, windowSize: ch.myWindow, maxPacketSize: ch.maxIncomingPayload, senderID: ch.localId }); if (await ch.ready.shift()) { return ch; } throw "failed to open"; } accept(): Promise { return this.incoming.shift(); } async close(): Promise { for (const ids of Object.keys(this.channels)) { let id = parseInt(ids); if (this.channels[id] !== undefined) { this.channels[id].shutdown(); } } await this.conn.close(); await this.done; } async loop() { try { while (true) { let msg = await this.dec.decode(); if (msg === undefined) { this.close(); return; } if (msg.ID === codec.OpenID) { await this.handleOpen(msg as codec.OpenMessage); continue; } let cmsg: codec.ChannelMessage = msg as codec.ChannelMessage; let ch = this.getCh(cmsg.channelID); if (ch === undefined) { throw `invalid channel (${cmsg.channelID}) on op ${cmsg.ID}`; } await ch.handle(cmsg); } } catch (e) { throw new Error(`session readloop: ${e}`); } // catch { // this.channels.forEach(async (ch) => { // await ch.close(); // }) // this.channels = []; // await this.conn.close(); // } } async handleOpen(msg: codec.OpenMessage) { if (msg.maxPacketSize < minPacketLength || msg.maxPacketSize > maxPacketLength) { await this.enc.encode({ ID: codec.OpenFailureID, channelID: msg.senderID }); return; } let c = this.newChannel(); c.remoteId = msg.senderID; c.maxRemotePayload = msg.maxPacketSize; c.remoteWin = msg.windowSize; c.maxIncomingPayload = internal.channelMaxPacket; this.incoming.push(c); await this.enc.encode({ ID: codec.OpenConfirmID, channelID: c.remoteId, senderID: c.localId, windowSize: c.myWindow, maxPacketSize: c.maxIncomingPayload }); } newChannel(): internal.Channel { let ch = new internal.Channel(this); ch.remoteWin = 0; ch.myWindow = internal.channelWindowSize; ch.localId = this.addCh(ch); return ch; } getCh(id: number): internal.Channel { let ch = this.channels[id]; if (ch && ch.localId !== id) { console.log("bad ids:", id, ch.localId, ch.remoteId); } return ch; } addCh(ch: internal.Channel): number { this.channels.forEach((v, i) => { if (v === undefined) { this.channels[i] = ch; return i; } }); this.channels.push(ch); return this.channels.length - 1; } rmCh(id: number): void { delete this.channels[id]; } } ================================================ FILE: typescript/session_test.ts ================================================ import { assertEquals, } from "https://deno.land/std/testing/asserts.ts"; import * as session from "./session.ts"; import * as api from "./api.ts"; import * as util from "./util.ts"; import * as tcp from "./transport/deno/tcp.ts"; import * as websocket from "./transport/deno/websocket.ts"; async function readAll(conn: api.IConn): Promise { let buff = new Uint8Array(); while (true) { let next = await conn.read(100); if (next === undefined) { return buff; } buff = util.concat([buff, next], buff.byteLength + next.byteLength); } } async function startListener(listener: api.IConnListener) { let conn = await listener.accept(); if (!conn) { throw new Error("accept failed") } let sess = new session.Session(conn); let ch = await sess.open(); let b = await readAll(ch); await ch.close(); let ch2 = await sess.accept(); if (ch2 === undefined) { throw new Error("accept failed") } await ch2.write(b); await ch2.close(); try { await sess.close(); await listener.close(); } catch (e) { console.log(e); } } async function testExchange(conn: api.IConn) { let sess = new session.Session(conn); let ch = await sess.accept(); if (ch === undefined) { throw new Error("accept failed") } await ch.write(new TextEncoder().encode("Hello world")); await ch.closeWrite(); await ch.close(); let ch2 = await sess.open(); let b = await readAll(ch2); await ch2.close(); assertEquals(new TextEncoder().encode("Hello world"), b); try { await sess.close(); } catch (e) { console.log(e); } } Deno.test("tcp", async () => { let listener = new tcp.Listener({ port: 0 }); let port = (listener.listener.addr as Deno.NetAddr).port; await Promise.all([ startListener(listener), tcp.Dial({ port }).then(conn => { return testExchange(conn); }), ]); }); Deno.test("websocket", async () => { let endpoint = "ws://127.0.0.1:9999"; let listener = new websocket.Listener(9999); await Promise.all([ startListener(listener), websocket.Dial(endpoint).then(conn => { return testExchange(conn); }), ]); }); Deno.test("multiple pending reads", async () => { let listener = Deno.listen({ port: 0 }); let port = (listener.addr as Deno.NetAddr).port; let lConn = listener.accept(); let sess1 = new session.Session(new tcp.Conn(await Deno.connect({ port }))); let sess2 = new session.Session(new tcp.Conn(await lConn)); let ch1p = sess1.accept(); let ch2 = await sess2.open(); let ch1 = await ch1p; if (ch1 === undefined) { throw new Error("accept failed"); } let a = ch1.read(1); let bc = ch1.read(2); await ch2.write(new TextEncoder().encode("abc")); assertEquals(await a, new TextEncoder().encode("a")) assertEquals(await bc, new TextEncoder().encode("bc")) await ch2.closeWrite(); await ch2.close(); await sess2.close(); await ch1.close(); await sess1.close(); listener.close(); }); ================================================ FILE: typescript/transport/deno/tcp.ts ================================================ // @ts-ignore import * as api from "../../api.ts"; export class Listener implements api.IConnListener { listener: Deno.Listener; constructor(opts: Deno.ListenOptions) { this.listener = Deno.listen(opts); } async accept(): Promise { return new Conn(await this.listener.accept()); } close(): Promise { this.listener.close(); return Promise.resolve(); } } export async function Dial(opts: Deno.ConnectOptions): Promise { return new Conn(await Deno.connect(opts)); } export class Conn implements api.IConn { conn: Deno.Conn; constructor(conn: Deno.Conn) { this.conn = conn; } async read(len: number): Promise { let buff = new Uint8Array(len); let n: number | null; try { n = await this.conn.read(buff); } catch (e) { if (e instanceof Deno.errors.Interrupted || e instanceof Deno.errors.BadResource) { return undefined; } throw e; } if (n == null) { return undefined; } if (buff.byteLength > n) { buff = buff.slice(0, n); } return buff; } write(buffer: Uint8Array): Promise { return this.conn.write(buffer) } close(): Promise { try { this.conn.close(); } catch (e) { if (!(e instanceof Deno.errors.BadResource)) { throw e; } } return Promise.resolve(); } } ================================================ FILE: typescript/transport/deno/websocket.ts ================================================ import { StandardWebSocketClient, WebSocketClient, WebSocketServer } from "https://deno.land/x/websocket@v0.1.2/mod.ts"; // @ts-ignore import * as api from "./../../api.ts"; // @ts-ignore import * as internal from "./../../internal.ts"; // @ts-ignore import * as util from "./../../util.ts"; export class Listener implements api.IConnListener { wss: WebSocketServer q: util.queue constructor(port: number) { this.q = new util.queue(); this.wss = new WebSocketServer(port); this.wss.on("connection", (ws: WebSocketClient) => { this.q.push(new Conn(ws)); }) } accept(): Promise { return this.q.shift(); } async close(): Promise { await this.wss.close(); this.q.close(); } } export function Dial(endpoint: string): Promise { let ws = new StandardWebSocketClient(endpoint); return new Promise((resolve) => { // TODO errors? ws.on("open", function () { resolve(new Conn(ws)); }); }) } export class Conn implements api.IConn { socket: WebSocketClient buf: util.ReadBuffer isClosed: boolean constructor(socket: WebSocketClient) { this.isClosed = false; this.socket = socket; this.buf = new util.ReadBuffer(); this.socket.on("message", (event: MessageEvent | Uint8Array) => { if (event instanceof Uint8Array) { this.buf.write(event); return; } event.data.arrayBuffer().then((data) => { let buf = new Uint8Array(data); this.buf.write(buf); }); }); this.socket.on("close", () => { this.close(); }); //this.socket.onerror = (err) => console.error("qtalk", err); } read(len: number): Promise { return this.buf.read(len); } write(buffer: Uint8Array): Promise { this.socket.send(buffer); return Promise.resolve(buffer.byteLength); } async close(): Promise { if (this.isClosed) { return; } this.isClosed = true; this.buf.close(); await this.socket.close(1000); // Code 1000: Normal Closure } } ================================================ FILE: typescript/transport/websocket.ts ================================================ // @ts-ignore import * as api from "./../api.ts"; // @ts-ignore import * as internal from "./../internal.ts"; // @ts-ignore import * as util from "./../util.ts"; export function Dial(addr: string, debug: boolean = false, onclose?: () => void): Promise { return new Promise((resolve) => { var socket = new WebSocket(addr); socket.onopen = () => resolve(new internal.Session(new Conn(socket), debug)); //socket.onerror = (err) => console.error("qtalk", err); if (onclose) socket.onclose = onclose; }) } export class Conn implements api.IConn { socket: WebSocket error: any waiters: Array<() => void> buf: Uint8Array; isClosed: boolean constructor(socket: WebSocket) { this.isClosed = false; this.buf = new Uint8Array(0); this.waiters = []; this.socket = socket; this.socket.binaryType = "arraybuffer"; this.socket.onmessage = (event) => { var buf = new Uint8Array(event.data); this.buf = util.concat([this.buf, buf], this.buf.length + buf.length); if (this.waiters.length > 0) { let waiter = this.waiters.shift(); if (waiter) waiter(); } }; let onclose = this.socket.onclose; this.socket.onclose = (e: CloseEvent) => { if (onclose) onclose.bind(this.socket)(e); this.close(); } //this.socket.onerror = (err) => console.error("qtalk", err); } read(len: number): Promise { return new Promise((resolve) => { var tryRead = () => { if (this.isClosed) { resolve(undefined); return; } if (this.buf.length >= len) { var data = this.buf.slice(0, len); this.buf = this.buf.slice(len); resolve(data); return; } this.waiters.push(tryRead); } tryRead(); }) } write(buffer: Uint8Array): Promise { this.socket.send(buffer); return Promise.resolve(buffer.byteLength); } close(): Promise { if (this.isClosed) return Promise.resolve(); return new Promise((resolve) => { this.isClosed = true; this.waiters.forEach(waiter => waiter()); this.socket.close(); resolve(); }); } } ================================================ FILE: typescript/tsconfig.json ================================================ { "compilerOptions": { "lib": ["es2016", "dom", "es5"], "noImplicitAny": true, "allowJs": true, } } ================================================ FILE: typescript/util.ts ================================================ export function concat(list: Uint8Array[], totalLength: number): Uint8Array { let buf = new Uint8Array(totalLength); let offset = 0; list.forEach((el) => { buf.set(el, offset); offset += el.length; }); return buf; } // queue primitive for incoming connections and // signaling channel ready state export class queue { q: Array waiters: Array<(a: ValueType | undefined) => void> closed: boolean constructor() { this.q = []; this.waiters = []; this.closed = false; } push(obj: ValueType) { if (this.closed) throw "closed queue"; if (this.waiters.length > 0) { let waiter = this.waiters.shift() if (waiter) waiter(obj); return; } this.q.push(obj); } shift(): Promise { if (this.closed) return Promise.resolve(undefined); return new Promise(resolve => { if (this.q.length > 0) { resolve(this.q.shift()); return; } this.waiters.push(resolve); }) } close() { if (this.closed) return; this.closed = true; this.waiters.forEach(waiter => { waiter(undefined); }); } } export class ReadBuffer { gotEOF: boolean; readBuf: Uint8Array | undefined; readers: Array<() => void>; constructor() { this.readBuf = new Uint8Array(0); this.gotEOF = false; this.readers = []; } read(len: number): Promise { return new Promise(resolve => { let tryRead = () => { if (this.readBuf === undefined) { resolve(undefined); return; } if (this.readBuf.length == 0) { if (this.gotEOF) { this.readBuf = undefined; resolve(undefined); return; } this.readers.push(tryRead); return; } let data = this.readBuf.slice(0, len); this.readBuf = this.readBuf.slice(data.byteLength); if (this.readBuf.length == 0 && this.gotEOF) { this.readBuf = undefined; } resolve(data); } tryRead(); }); } write(data: Uint8Array) { if (this.readBuf) { this.readBuf = concat([this.readBuf, data], this.readBuf.length + data.length); } while (!this.readBuf || this.readBuf.length > 0) { let reader = this.readers.shift(); if (!reader) break reader(); } } eof() { this.gotEOF = true; this.flushReaders(); } close() { this.readBuf = undefined; this.flushReaders(); } protected flushReaders() { while (true) { let reader = this.readers.shift(); if (reader === undefined) { return; } reader(); } } }