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=<server hostname>] <local port>
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=<hostname>] [-b=<bind ip>]
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, "<<ENC", msg)
}
b, err := Marshal(msg)
if err != nil {
return err
}
if DebugBytes != nil {
fmt.Fprintln(DebugBytes, "<<ENC", b)
}
_, err = enc.w.Write(b)
return err
}
type Marshaler interface {
MarshalMux() ([]byte, error)
}
func Marshal(v interface{}) ([]byte, error) {
m, ok := v.(Marshaler)
if !ok {
return []byte{}, fmt.Errorf("qmux: unable to marshal type")
}
return m.MarshalMux()
}
================================================
FILE: golang/codec/message.go
================================================
package codec
const (
msgChannelOpen = iota + 100
msgChannelOpenConfirm
msgChannelOpenFailure
msgChannelWindowAdjust
msgChannelData
msgChannelEOF
msgChannelClose
)
var (
payloadSizes = map[byte]int{
msgChannelOpen: 12,
msgChannelOpenConfirm: 16,
msgChannelOpenFailure: 4,
msgChannelWindowAdjust: 8,
msgChannelData: 8,
msgChannelEOF: 4,
msgChannelClose: 4,
}
)
type Message interface {
Channel() (uint32, bool)
String() string
}
================================================
FILE: golang/codec/message_close.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type CloseMessage struct {
ChannelID uint32
}
func (msg CloseMessage) String() string {
return fmt.Sprintf("{CloseMessage ChannelID:%d}", msg.ChannelID)
}
func (msg CloseMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg CloseMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelClose]+1)
packet[0] = msgChannelClose
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
return packet, nil
}
func (msg *CloseMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
return nil
}
================================================
FILE: golang/codec/message_data.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type DataMessage struct {
ChannelID uint32
Length uint32
Data []byte
}
func (msg DataMessage) String() string {
return fmt.Sprintf("{DataMessage ChannelID:%d Length:%d Data: ... }",
msg.ChannelID, msg.Length)
}
func (msg DataMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg DataMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelData]+1)
packet[0] = msgChannelData
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
binary.BigEndian.PutUint32(packet[5:9], msg.Length)
return append(packet, msg.Data...), nil
}
func (msg *DataMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
msg.Length = binary.BigEndian.Uint32(b[5:9])
msg.Data = b[9:]
return nil
}
================================================
FILE: golang/codec/message_eof.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type EOFMessage struct {
ChannelID uint32
}
func (msg EOFMessage) String() string {
return fmt.Sprintf("{EOFMessage ChannelID:%d}", msg.ChannelID)
}
func (msg EOFMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg EOFMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelEOF]+1)
packet[0] = msgChannelEOF
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
return packet, nil
}
func (msg *EOFMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
return nil
}
================================================
FILE: golang/codec/message_open.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type OpenMessage struct {
SenderID uint32
WindowSize uint32
MaxPacketSize uint32
}
func (msg OpenMessage) String() string {
return fmt.Sprintf("{OpenMessage SenderID:%d WindowSize:%d MaxPacketSize:%d}",
msg.SenderID, msg.WindowSize, msg.MaxPacketSize)
}
func (msg OpenMessage) Channel() (uint32, bool) {
return 0, false
}
func (msg OpenMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelOpen]+1)
packet[0] = msgChannelOpen
binary.BigEndian.PutUint32(packet[1:5], msg.SenderID)
binary.BigEndian.PutUint32(packet[5:9], msg.WindowSize)
binary.BigEndian.PutUint32(packet[9:13], msg.MaxPacketSize)
return packet, nil
}
func (msg *OpenMessage) UnmarshalMux(b []byte) error {
msg.SenderID = binary.BigEndian.Uint32(b[1:5])
msg.WindowSize = binary.BigEndian.Uint32(b[5:9])
msg.MaxPacketSize = binary.BigEndian.Uint32(b[9:13])
return nil
}
================================================
FILE: golang/codec/message_openconfirm.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type OpenConfirmMessage struct {
ChannelID uint32
SenderID uint32
WindowSize uint32
MaxPacketSize uint32
}
func (msg OpenConfirmMessage) String() string {
return fmt.Sprintf("{OpenConfirmMessage ChannelID:%d SenderID:%d WindowSize:%d MaxPacketSize:%d}",
msg.ChannelID, msg.SenderID, msg.WindowSize, msg.MaxPacketSize)
}
func (msg OpenConfirmMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg OpenConfirmMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelOpenConfirm]+1)
packet[0] = msgChannelOpenConfirm
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
binary.BigEndian.PutUint32(packet[5:9], msg.SenderID)
binary.BigEndian.PutUint32(packet[9:13], msg.WindowSize)
binary.BigEndian.PutUint32(packet[13:17], msg.MaxPacketSize)
return packet, nil
}
func (msg *OpenConfirmMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
msg.SenderID = binary.BigEndian.Uint32(b[5:9])
msg.WindowSize = binary.BigEndian.Uint32(b[9:13])
msg.MaxPacketSize = binary.BigEndian.Uint32(b[13:17])
return nil
}
================================================
FILE: golang/codec/message_openfailure.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type OpenFailureMessage struct {
ChannelID uint32
}
func (msg OpenFailureMessage) String() string {
return fmt.Sprintf("{OpenFailureMessage ChannelID:%d}", msg.ChannelID)
}
func (msg OpenFailureMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg OpenFailureMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelOpenFailure]+1)
packet[0] = msgChannelOpenFailure
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
return packet, nil
}
func (msg *OpenFailureMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
return nil
}
================================================
FILE: golang/codec/message_windowadjust.go
================================================
package codec
import (
"encoding/binary"
"fmt"
)
type WindowAdjustMessage struct {
ChannelID uint32
AdditionalBytes uint32
}
func (msg WindowAdjustMessage) String() string {
return fmt.Sprintf("{WindowAdjustMessage ChannelID:%d AdditionalBytes:%d}",
msg.ChannelID, msg.AdditionalBytes)
}
func (msg WindowAdjustMessage) Channel() (uint32, bool) {
return msg.ChannelID, true
}
func (msg WindowAdjustMessage) MarshalMux() ([]byte, error) {
packet := make([]byte, payloadSizes[msgChannelWindowAdjust]+1)
packet[0] = msgChannelWindowAdjust
binary.BigEndian.PutUint32(packet[1:5], msg.ChannelID)
binary.BigEndian.PutUint32(packet[5:9], msg.AdditionalBytes)
return packet, nil
}
func (msg *WindowAdjustMessage) UnmarshalMux(b []byte) error {
msg.ChannelID = binary.BigEndian.Uint32(b[1:5])
msg.AdditionalBytes = binary.BigEndian.Uint32(b[5:9])
return nil
}
================================================
FILE: golang/go.mod
================================================
module github.com/progrium/qmux/golang
go 1.16
require golang.org/x/net v0.0.0-20210420210106-798c2154c571 // indirect
================================================
FILE: golang/go.sum
================================================
golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg=
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: golang/mux/api.go
================================================
package mux
import (
"context"
"io"
)
// Session is a bi-directional channel muxing session on a given transport.
type Session interface {
// Close closes the underlying transport.
// Any blocked Accept operations will be unblocked and return errors.
Close() error
// Open establishes a new channel with the other end.
Open(ctx context.Context) (Channel, error)
// Accept waits for and returns the next incoming channel.
Accept() (Channel, error)
}
// Channel is an ordered, reliable, flow-controlled, duplex stream
// that is multiplexed over a qmux session.
type Channel interface {
// Read reads up to len(data) bytes from the channel.
Read(data []byte) (int, error)
// Write writes len(data) bytes to the channel.
Write(data []byte) (int, error)
// Close signals end of channel use. No data may be sent after this
// call.
Close() error
// CloseWrite signals the end of sending data.
// The other side may still send data
CloseWrite() error
// ID returns the unique identifier of this channel
// within the session
ID() uint32
}
// Transport is an interface describing what is needed for a session
type Transport interface {
io.Reader
io.Writer
io.Closer
}
================================================
FILE: golang/mux/doc.go
================================================
// Package mux provides a generic muxing API.
package mux
================================================
FILE: golang/mux/misc.go
================================================
package mux
import "fmt"
type waiter interface {
Wait() error
}
// Wait blocks until the session transport has shut down, and returns the
// error causing the shutdown.
func Wait(sess Session) error {
w, ok := sess.(waiter)
if !ok {
return fmt.Errorf("Session does not support waiting")
}
return w.Wait()
}
================================================
FILE: golang/session/channel.go
================================================
package session
import (
"errors"
"fmt"
"io"
"sync"
"github.com/progrium/qmux/golang/codec"
)
type channelDirection uint8
const (
channelInbound channelDirection = iota
channelOutbound
)
// Channel is an implementation of the Channel interface that works
// with the Session class.
type Channel struct {
// R/O after creation
localId, remoteId uint32
// maxIncomingPayload and maxRemotePayload are the maximum
// payload sizes of normal and extended data packets for
// receiving and sending, respectively. The wire packet will
// be 9 or 13 bytes larger (excluding encryption overhead).
maxIncomingPayload uint32
maxRemotePayload uint32
session *Session
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
// Pending internal channel messages.
msg chan codec.Message
sentEOF bool
// thread-safe data
remoteWin window
pending *buffer
// windowMu protects myWindow, the flow-control window.
windowMu sync.Mutex
myWindow uint32
// writeMu serializes calls to session.conn.Write() and
// protects sentClose and packetPool. This mutex must be
// different from windowMu, as writePacket can block if there
// is a key exchange pending.
writeMu sync.Mutex
sentClose bool
// packet buffer for writing
packetBuf []byte
}
// ID returns the unique identifier of this channel
// within the session
func (ch *Channel) ID() uint32 {
return ch.localId
}
// CloseWrite signals the end of sending data.
// The other side may still send data
func (ch *Channel) CloseWrite() error {
ch.sentEOF = true
return ch.send(codec.EOFMessage{
ChannelID: ch.remoteId})
}
// Close signals end of channel use. No data may be sent after this
// call.
func (ch *Channel) Close() error {
return ch.send(codec.CloseMessage{
ChannelID: ch.remoteId})
}
// Write writes len(data) bytes to the channel.
func (ch *Channel) Write(data []byte) (n int, err error) {
if ch.sentEOF {
return 0, io.EOF
}
for len(data) > 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<Uint8Array | undefined>;
write(buffer: Uint8Array): Promise<number>;
close(): Promise<void>;
}
export interface ISession {
open(): Promise<IChannel>;
accept(): Promise<IChannel | undefined>;
close(): Promise<void>;
}
export interface IChannel extends IConn {
ident(): number
closeWrite(): Promise<void>
}
export interface IConnListener {
accept(): Promise<IConn | undefined>;
close(): Promise<void>;
}
================================================
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<boolean>;
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<Uint8Array | undefined> {
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<number> {
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<void> {
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<number> {
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<msg.Message | undefined> {
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<Uint8Array | undefined> {
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<number> {
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<internal.Channel>;
incoming: util.queue<api.IChannel>;
enc: codec.Encoder;
dec: codec.Decoder;
done: Promise<void>;
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<api.IChannel> {
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<api.IChannel | undefined> {
return this.incoming.shift();
}
async close(): Promise<void> {
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<Uint8Array> {
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<Conn | undefined> {
return new Conn(await this.listener.accept());
}
close(): Promise<void> {
this.listener.close();
return Promise.resolve();
}
}
export async function Dial(opts: Deno.ConnectOptions): Promise<Conn> {
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<Uint8Array | undefined> {
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<number> {
return this.conn.write(buffer)
}
close(): Promise<void> {
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<Conn>
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<Conn | undefined> {
return this.q.shift();
}
async close(): Promise<void> {
await this.wss.close();
this.q.close();
}
}
export function Dial(endpoint: string): Promise<Conn> {
let ws = new StandardWebSocketClient(endpoint);
return new Promise<Conn>((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<Blob> | 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<Uint8Array | undefined> {
return this.buf.read(len);
}
write(buffer: Uint8Array): Promise<number> {
this.socket.send(buffer);
return Promise.resolve(buffer.byteLength);
}
async close(): Promise<void> {
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<api.ISession> {
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<Uint8Array | undefined> {
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<number> {
this.socket.send(buffer);
return Promise.resolve(buffer.byteLength);
}
close(): Promise<void> {
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<ValueType> {
q: Array<ValueType>
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<ValueType | undefined> {
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<Uint8Array | undefined> {
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();
}
}
}
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
SYMBOL INDEX (236 symbols across 40 files)
FILE: demos/groktunnel/main.go
function main (line 21) | func main() {
function serve (line 71) | func serve(vmux *vhost.HTTPMuxer, host, port string) {
function join (line 106) | func join(a io.ReadWriteCloser, b io.ReadWriteCloser) {
function newSubdomain (line 113) | func newSubdomain() string {
function fatal (line 126) | func fatal(err error) {
FILE: golang/codec/codec_test.go
function TestMarshalUnmarshal (line 8) | func TestMarshalUnmarshal(t *testing.T) {
function TestEncodeDecode (line 86) | func TestEncodeDecode(t *testing.T) {
FILE: golang/codec/decoder.go
type Decoder (line 13) | type Decoder struct
method Decode (line 22) | func (dec *Decoder) Decode() (Message, error) {
function NewDecoder (line 18) | func NewDecoder(r io.Reader) *Decoder {
function readPacket (line 38) | func readPacket(c io.Reader) ([]byte, error) {
function decode (line 71) | func decode(packet []byte) (Message, error) {
type Unmarshaler (line 100) | type Unmarshaler interface
function Unmarshal (line 104) | func Unmarshal(b []byte, v interface{}) error {
FILE: golang/codec/encoder.go
type Encoder (line 9) | type Encoder struct
method Encode (line 18) | func (enc *Encoder) Encode(msg interface{}) error {
function NewEncoder (line 14) | func NewEncoder(w io.Writer) *Encoder {
type Marshaler (line 39) | type Marshaler interface
function Marshal (line 43) | func Marshal(v interface{}) ([]byte, error) {
FILE: golang/codec/message.go
constant msgChannelOpen (line 4) | msgChannelOpen = iota + 100
constant msgChannelOpenConfirm (line 5) | msgChannelOpenConfirm
constant msgChannelOpenFailure (line 6) | msgChannelOpenFailure
constant msgChannelWindowAdjust (line 7) | msgChannelWindowAdjust
constant msgChannelData (line 8) | msgChannelData
constant msgChannelEOF (line 9) | msgChannelEOF
constant msgChannelClose (line 10) | msgChannelClose
type Message (line 25) | type Message interface
FILE: golang/codec/message_close.go
type CloseMessage (line 8) | type CloseMessage struct
method String (line 12) | func (msg CloseMessage) String() string {
method Channel (line 16) | func (msg CloseMessage) Channel() (uint32, bool) {
method MarshalMux (line 20) | func (msg CloseMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 27) | func (msg *CloseMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_data.go
type DataMessage (line 8) | type DataMessage struct
method String (line 14) | func (msg DataMessage) String() string {
method Channel (line 19) | func (msg DataMessage) Channel() (uint32, bool) {
method MarshalMux (line 23) | func (msg DataMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 31) | func (msg *DataMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_eof.go
type EOFMessage (line 8) | type EOFMessage struct
method String (line 12) | func (msg EOFMessage) String() string {
method Channel (line 16) | func (msg EOFMessage) Channel() (uint32, bool) {
method MarshalMux (line 20) | func (msg EOFMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 27) | func (msg *EOFMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_open.go
type OpenMessage (line 8) | type OpenMessage struct
method String (line 14) | func (msg OpenMessage) String() string {
method Channel (line 19) | func (msg OpenMessage) Channel() (uint32, bool) {
method MarshalMux (line 23) | func (msg OpenMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 32) | func (msg *OpenMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_openconfirm.go
type OpenConfirmMessage (line 8) | type OpenConfirmMessage struct
method String (line 15) | func (msg OpenConfirmMessage) String() string {
method Channel (line 20) | func (msg OpenConfirmMessage) Channel() (uint32, bool) {
method MarshalMux (line 24) | func (msg OpenConfirmMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 34) | func (msg *OpenConfirmMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_openfailure.go
type OpenFailureMessage (line 8) | type OpenFailureMessage struct
method String (line 12) | func (msg OpenFailureMessage) String() string {
method Channel (line 16) | func (msg OpenFailureMessage) Channel() (uint32, bool) {
method MarshalMux (line 20) | func (msg OpenFailureMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 27) | func (msg *OpenFailureMessage) UnmarshalMux(b []byte) error {
FILE: golang/codec/message_windowadjust.go
type WindowAdjustMessage (line 8) | type WindowAdjustMessage struct
method String (line 13) | func (msg WindowAdjustMessage) String() string {
method Channel (line 18) | func (msg WindowAdjustMessage) Channel() (uint32, bool) {
method MarshalMux (line 22) | func (msg WindowAdjustMessage) MarshalMux() ([]byte, error) {
method UnmarshalMux (line 30) | func (msg *WindowAdjustMessage) UnmarshalMux(b []byte) error {
FILE: golang/mux/api.go
type Session (line 9) | type Session interface
type Channel (line 23) | type Channel interface
type Transport (line 44) | type Transport interface
FILE: golang/mux/misc.go
type waiter (line 5) | type waiter interface
function Wait (line 11) | func Wait(sess Session) error {
FILE: golang/session/channel.go
type channelDirection (line 12) | type channelDirection
constant channelInbound (line 15) | channelInbound channelDirection = iota
constant channelOutbound (line 16) | channelOutbound
type Channel (line 21) | type Channel struct
method ID (line 65) | func (ch *Channel) ID() uint32 {
method CloseWrite (line 71) | func (ch *Channel) CloseWrite() error {
method Close (line 79) | func (ch *Channel) Close() error {
method Write (line 85) | func (ch *Channel) Write(data []byte) (n int, err error) {
method Read (line 114) | func (c *Channel) Read(data []byte) (n int, err error) {
method send (line 132) | func (ch *Channel) send(msg interface{}) error {
method adjustWindow (line 147) | func (c *Channel) adjustWindow(n uint32) error {
method close (line 159) | func (c *Channel) close() {
method responseMessageReceived (line 174) | func (ch *Channel) responseMessageReceived() error {
method handle (line 181) | func (ch *Channel) handle(msg codec.Message) error {
method handleData (line 230) | func (ch *Channel) handleData(msg *codec.DataMessage) error {
FILE: golang/session/session.go
constant minPacketLength (line 14) | minPacketLength = 9
constant maxPacketLength (line 15) | maxPacketLength = 1 << 31
constant channelMaxPacket (line 20) | channelMaxPacket = 1 << 15
constant channelWindowSize (line 22) | channelWindowSize = 64 * channelMaxPacket
constant chanSize (line 27) | chanSize = 16
type Session (line 31) | type Session struct
method Close (line 63) | func (s *Session) Close() error {
method Wait (line 70) | func (s *Session) Wait() error {
method Accept (line 80) | func (s *Session) Accept() (mux.Channel, error) {
method Open (line 90) | func (s *Session) Open(ctx context.Context) (mux.Channel, error) {
method newChannel (line 123) | func (s *Session) newChannel(direction channelDirection) *Channel {
method loop (line 139) | func (s *Session) loop() {
method onePacket (line 159) | func (s *Session) onePacket() error {
method handleOpen (line 182) | func (s *Session) handleOpen(msg *codec.OpenMessage) error {
function New (line 46) | func New(t mux.Transport) *Session {
FILE: golang/session/session_test.go
function fatal (line 15) | func fatal(err error, t *testing.T) {
function TestQmux (line 22) | func TestQmux(t *testing.T) {
function TestSessionOpenTimeout (line 86) | func TestSessionOpenTimeout(t *testing.T) {
function TestSessionWait (line 109) | func TestSessionWait(t *testing.T) {
FILE: golang/session/util.go
function min (line 3) | func min(a uint32, b int) uint32 {
FILE: golang/session/util_buffer.go
type buffer (line 11) | type buffer struct
method write (line 40) | func (b *buffer) write(buf []byte) {
method eof (line 51) | func (b *buffer) eof() {
method Read (line 60) | func (b *buffer) Read(buf []byte) (n int, err error) {
type element (line 22) | type element struct
function newBuffer (line 28) | func newBuffer() *buffer {
FILE: golang/session/util_chanlist.go
type chanList (line 6) | type chanList struct
method add (line 16) | func (c *chanList) add(ch *Channel) uint32 {
method getChan (line 30) | func (c *chanList) getChan(id uint32) *Channel {
method remove (line 39) | func (c *chanList) remove(id uint32) {
method dropAll (line 48) | func (c *chanList) dropAll() []*Channel {
FILE: golang/session/util_window.go
type window (line 10) | type window struct
method add (line 19) | func (w *window) add(win uint32) bool {
method close (line 40) | func (w *window) close() {
method reserve (line 50) | func (w *window) reserve(win uint32) (uint32, error) {
method waitWriterBlocked (line 72) | func (w *window) waitWriterBlocked() {
FILE: golang/transport/dial_io.go
function DialIO (line 11) | func DialIO(out io.WriteCloser, in io.ReadCloser) (mux.Session, error) {
function DialStdio (line 15) | func DialStdio() (mux.Session, error) {
FILE: golang/transport/dial_net.go
function dialNet (line 10) | func dialNet(proto, addr string) (mux.Session, error) {
function DialTCP (line 18) | func DialTCP(addr string) (mux.Session, error) {
function DialUnix (line 22) | func DialUnix(addr string) (mux.Session, error) {
FILE: golang/transport/dial_ws.go
function DialWS (line 11) | func DialWS(addr string) (mux.Session, error) {
FILE: golang/transport/listen.go
type Listener (line 5) | type Listener interface
FILE: golang/transport/listen_io.go
type IOListener (line 11) | type IOListener struct
method Accept (line 15) | func (l *IOListener) Accept() (mux.Session, error) {
type ioduplex (line 19) | type ioduplex struct
method Close (line 24) | func (d *ioduplex) Close() error {
function ListenIO (line 34) | func ListenIO(out io.WriteCloser, in io.ReadCloser) (*IOListener, error) {
function ListenStdio (line 40) | func ListenStdio() (*IOListener, error) {
FILE: golang/transport/listen_net.go
type NetListener (line 11) | type NetListener struct
method Accept (line 18) | func (l *NetListener) Accept() (mux.Session, error) {
method Close (line 33) | func (l *NetListener) Close() error {
function listenNet (line 40) | func listenNet(proto, addr string) (*NetListener, error) {
function ListenTCP (line 66) | func ListenTCP(addr string) (*NetListener, error) {
function ListenUnix (line 70) | func ListenUnix(addr string) (*NetListener, error) {
FILE: golang/transport/listen_ws.go
function HandleWS (line 12) | func HandleWS(l *NetListener, ws *websocket.Conn) {
function ListenWS (line 20) | func ListenWS(addr string) (*NetListener, error) {
FILE: golang/transport/transport_test.go
function fatal (line 14) | func fatal(err error, t *testing.T) {
function testExchange (line 21) | func testExchange(t *testing.T, sess mux.Session) {
function startListener (line 59) | func startListener(t *testing.T, l Listener) {
function TestTCP (line 96) | func TestTCP(t *testing.T) {
function TestUnix (line 106) | func TestUnix(t *testing.T) {
function TestIO (line 118) | func TestIO(t *testing.T) {
function TestWS (line 131) | func TestWS(t *testing.T) {
FILE: typescript/api.ts
type IConn (line 2) | interface IConn {
type ISession (line 8) | interface ISession {
type IChannel (line 14) | interface IChannel extends IConn {
type IConnListener (line 19) | interface IConnListener {
FILE: typescript/channel.ts
class Channel (line 12) | class Channel {
method constructor (line 26) | constructor(sess: internal.Session) {
method ident (line 41) | ident(): number {
method read (line 45) | async read(len: number): Promise<Uint8Array | undefined> {
method reserveWindow (line 59) | reserveWindow(win: number): number {
method addWindow (line 67) | addWindow(win: number) {
method write (line 76) | write(buffer: Uint8Array): Promise<number> {
method closeWrite (line 120) | async closeWrite() {
method close (line 130) | async close(): Promise<void> {
method shutdown (line 143) | shutdown(): void {
method adjustWindow (line 150) | async adjustWindow(n: number) {
method send (line 161) | send(msg: codec.ChannelMessage): Promise<number> {
method handle (line 171) | handle(msg: codec.ChannelMessage): void {
method handleData (line 203) | handleData(msg: codec.DataMessage) {
FILE: typescript/codec/decoder.ts
class Decoder (line 8) | class Decoder {
method constructor (line 12) | constructor(conn: api.IConn, debug: boolean = false) {
method decode (line 17) | async decode(): Promise<msg.Message | undefined> {
function readPacket (line 30) | async function readPacket(conn: api.IConn): Promise<Uint8Array | undefin...
function Unmarshal (line 60) | function Unmarshal(packet: Uint8Array): msg.Message {
FILE: typescript/codec/encoder.ts
class Encoder (line 6) | class Encoder {
method constructor (line 10) | constructor(conn: api.IConn, debug: boolean = false) {
method encode (line 15) | async encode(m: msg.AnyMessage): Promise<number> {
function Marshal (line 23) | function Marshal(obj: msg.AnyMessage): Uint8Array {
FILE: typescript/codec/message.ts
type Message (line 20) | interface Message {
type OpenMessage (line 24) | interface OpenMessage {
type OpenConfirmMessage (line 31) | interface OpenConfirmMessage {
type OpenFailureMessage (line 39) | interface OpenFailureMessage {
type WindowAdjustMessage (line 44) | interface WindowAdjustMessage {
type DataMessage (line 50) | interface DataMessage {
type EOFMessage (line 57) | interface EOFMessage {
type CloseMessage (line 62) | interface CloseMessage {
type ChannelMessage (line 67) | type ChannelMessage = (
type AnyMessage (line 75) | type AnyMessage = ChannelMessage | OpenMessage;
FILE: typescript/session.ts
class Session (line 14) | class Session implements api.ISession {
method constructor (line 22) | constructor(conn: api.IConn, debug: boolean = false) {
method open (line 31) | async open(): Promise<api.IChannel> {
method accept (line 46) | accept(): Promise<api.IChannel | undefined> {
method close (line 50) | async close(): Promise<void> {
method loop (line 61) | async loop() {
method handleOpen (line 94) | async handleOpen(msg: codec.OpenMessage) {
method newChannel (line 117) | newChannel(): internal.Channel {
method getCh (line 125) | getCh(id: number): internal.Channel {
method addCh (line 133) | addCh(ch: internal.Channel): number {
method rmCh (line 144) | rmCh(id: number): void {
FILE: typescript/session_test.ts
function readAll (line 11) | async function readAll(conn: api.IConn): Promise<Uint8Array> {
function startListener (line 22) | async function startListener(listener: api.IConnListener) {
function testExchange (line 46) | async function testExchange(conn: api.IConn) {
FILE: typescript/transport/deno/tcp.ts
class Listener (line 5) | class Listener implements api.IConnListener {
method constructor (line 8) | constructor(opts: Deno.ListenOptions) {
method accept (line 12) | async accept(): Promise<Conn | undefined> {
method close (line 16) | close(): Promise<void> {
function Dial (line 22) | async function Dial(opts: Deno.ConnectOptions): Promise<Conn> {
class Conn (line 26) | class Conn implements api.IConn {
method constructor (line 29) | constructor(conn: Deno.Conn) {
method read (line 33) | async read(len: number): Promise<Uint8Array | undefined> {
method write (line 53) | write(buffer: Uint8Array): Promise<number> {
method close (line 57) | close(): Promise<void> {
FILE: typescript/transport/deno/websocket.ts
class Listener (line 10) | class Listener implements api.IConnListener {
method constructor (line 14) | constructor(port: number) {
method accept (line 22) | accept(): Promise<Conn | undefined> {
method close (line 26) | async close(): Promise<void> {
function Dial (line 32) | function Dial(endpoint: string): Promise<Conn> {
class Conn (line 42) | class Conn implements api.IConn {
method constructor (line 47) | constructor(socket: WebSocketClient) {
method read (line 67) | read(len: number): Promise<Uint8Array | undefined> {
method write (line 71) | write(buffer: Uint8Array): Promise<number> {
method close (line 76) | async close(): Promise<void> {
FILE: typescript/transport/websocket.ts
function Dial (line 8) | function Dial(addr: string, debug: boolean = false, onclose?: () => void...
class Conn (line 17) | class Conn implements api.IConn {
method constructor (line 24) | constructor(socket: WebSocket) {
method read (line 46) | read(len: number): Promise<Uint8Array | undefined> {
method write (line 65) | write(buffer: Uint8Array): Promise<number> {
method close (line 70) | close(): Promise<void> {
FILE: typescript/util.ts
function concat (line 2) | function concat(list: Uint8Array[], totalLength: number): Uint8Array {
class queue (line 14) | class queue<ValueType> {
method constructor (line 19) | constructor() {
method push (line 25) | push(obj: ValueType) {
method shift (line 35) | shift(): Promise<ValueType | undefined> {
method close (line 46) | close() {
class ReadBuffer (line 55) | class ReadBuffer {
method constructor (line 60) | constructor() {
method read (line 66) | read(len: number): Promise<Uint8Array | undefined> {
method write (line 93) | write(data: Uint8Array) {
method eof (line 105) | eof() {
method close (line 110) | close() {
method flushReaders (line 115) | protected flushReaders() {
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 64,
"preview": "# These are supported funding model platforms\n\ngithub: progrium\n"
},
{
"path": ".gitignore",
"chars": 49,
"preview": "TODO\ntypescript/dist \ndemos/groktunnel/groktunnel"
},
{
"path": ".vscode/extensions.json",
"chars": 75,
"preview": "{\n \"recommendations\": [\n \"denoland.vscode-deno\",\n \"golang.go\"\n ]\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 71,
"preview": "{\n \"deno.enable\": true,\n \"deno.lint\": true,\n \"deno.unstable\": true\n}"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2021 Jeff Lindsay\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 1396,
"preview": "# qmux\n\nqmux is a wire protocol for multiplexing connections or streams into a single connection. It is based on the [SS"
},
{
"path": "SPEC.md",
"chars": 6292,
"preview": "# qmux\n\nqmux is a wire protocol for multiplexing connections or streams into a single connection.\nIt is a subset of the "
},
{
"path": "demos/groktunnel/README.md",
"chars": 1388,
"preview": "# groktunnel\n\nExpose localhost HTTP servers with a public URL\n\n## Build\n```\n$ go build\n```\n\n## Try it out\n\nFirst we run "
},
{
"path": "demos/groktunnel/go.mod",
"chars": 211,
"preview": "module github.com/progrium/qmux/demos/groktunnel\n\ngo 1.16\n\nrequire (\n\tgithub.com/inconshreveable/go-vhost v0.0.0-2016062"
},
{
"path": "demos/groktunnel/go.sum",
"chars": 1100,
"preview": "github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE=\ng"
},
{
"path": "demos/groktunnel/main.go",
"chars": 3001,
"preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/http"
},
{
"path": "golang/README.md",
"chars": 1394,
"preview": "# qmux for Go\n\nAn implementation of qmux for multiplexing any reliable `io.ReadWriteCloser`.\n\n## Using qmux in Go\n\n```\ng"
},
{
"path": "golang/codec/codec.go",
"chars": 154,
"preview": "// Package codec implements encoding and decoding of qmux messages.\npackage codec\n\nimport \"io\"\n\nvar (\n\tDebugMessages io."
},
{
"path": "golang/codec/codec_test.go",
"chars": 2610,
"preview": "package codec\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestMarshalUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tin Mess"
},
{
"path": "golang/codec/decoder.go",
"chars": 2075,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"syscall\"\n)\n\ntype Decoder struct {\n\tr i"
},
{
"path": "golang/codec/encoder.go",
"chars": 719,
"preview": "package codec\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n)\n\ntype Encoder struct {\n\tw io.Writer\n\tsync.Mutex\n}\n\nfunc NewEncoder(w io.W"
},
{
"path": "golang/codec/message.go",
"chars": 490,
"preview": "package codec\n\nconst (\n\tmsgChannelOpen = iota + 100\n\tmsgChannelOpenConfirm\n\tmsgChannelOpenFailure\n\tmsgChannelWindowAdjus"
},
{
"path": "golang/codec/message_close.go",
"chars": 633,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype CloseMessage struct {\n\tChannelID uint32\n}\n\nfunc (msg CloseMess"
},
{
"path": "golang/codec/message_data.go",
"chars": 834,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype DataMessage struct {\n\tChannelID uint32\n\tLength uint32\n\tData"
},
{
"path": "golang/codec/message_eof.go",
"chars": 617,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype EOFMessage struct {\n\tChannelID uint32\n}\n\nfunc (msg EOFMessage)"
},
{
"path": "golang/codec/message_open.go",
"chars": 948,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype OpenMessage struct {\n\tSenderID uint32\n\tWindowSize uint"
},
{
"path": "golang/codec/message_openconfirm.go",
"chars": 1174,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype OpenConfirmMessage struct {\n\tChannelID uint32\n\tSenderID "
},
{
"path": "golang/codec/message_openfailure.go",
"chars": 681,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype OpenFailureMessage struct {\n\tChannelID uint32\n}\n\nfunc (msg Ope"
},
{
"path": "golang/codec/message_windowadjust.go",
"chars": 878,
"preview": "package codec\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\ntype WindowAdjustMessage struct {\n\tChannelID uint32\n\tAddition"
},
{
"path": "golang/go.mod",
"chars": 121,
"preview": "module github.com/progrium/qmux/golang\n\ngo 1.16\n\nrequire golang.org/x/net v0.0.0-20210420210106-798c2154c571 // indirect"
},
{
"path": "golang/go.sum",
"chars": 718,
"preview": "golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg=\ngolang.org/x/net v0."
},
{
"path": "golang/mux/api.go",
"chars": 1196,
"preview": "package mux\n\nimport (\n\t\"context\"\n\t\"io\"\n)\n\n// Session is a bi-directional channel muxing session on a given transport.\nty"
},
{
"path": "golang/mux/doc.go",
"chars": 58,
"preview": "// Package mux provides a generic muxing API.\npackage mux\n"
},
{
"path": "golang/mux/misc.go",
"chars": 317,
"preview": "package mux\n\nimport \"fmt\"\n\ntype waiter interface {\n\tWait() error\n}\n\n// Wait blocks until the session transport has shut "
},
{
"path": "golang/session/channel.go",
"chars": 6033,
"preview": "package session\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/progrium/qmux/golang/codec\"\n)\n\ntype channelDirect"
},
{
"path": "golang/session/doc.go",
"chars": 78,
"preview": "// Package session implements a qmux session and channel API.\npackage session\n"
},
{
"path": "golang/session/session.go",
"chars": 4487,
"preview": "package session\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/progrium/qmux/golang/codec\"\n\t\"github.com/progriu"
},
{
"path": "golang/session/session_test.go",
"chars": 2395,
"preview": "package session\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/progrium/qm"
},
{
"path": "golang/session/util.go",
"chars": 106,
"preview": "package session\n\nfunc min(a uint32, b int) uint32 {\n\tif a < uint32(b) {\n\t\treturn a\n\t}\n\treturn uint32(b)\n}\n"
},
{
"path": "golang/session/util_buffer.go",
"chars": 2044,
"preview": "package session\n\nimport (\n\t\"io\"\n\t\"sync\"\n)\n\n// buffer provides a linked list buffer for data exchange\n// between producer"
},
{
"path": "golang/session/util_chanlist.go",
"chars": 1155,
"preview": "package session\n\nimport \"sync\"\n\n// chanList is a thread safe channel list.\ntype chanList struct {\n\t// protects concurren"
},
{
"path": "golang/session/util_window.go",
"chars": 1617,
"preview": "package session\n\nimport (\n\t\"io\"\n\t\"sync\"\n)\n\n// window represents the buffer available to clients\n// wishing to write to a"
},
{
"path": "golang/transport/dial_io.go",
"chars": 325,
"preview": "package transport\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang/sessio"
},
{
"path": "golang/transport/dial_net.go",
"chars": 448,
"preview": "package transport\n\nimport (\n\t\"net\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang/session\"\n)\n"
},
{
"path": "golang/transport/dial_ws.go",
"chars": 403,
"preview": "package transport\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang/session\"\n\t\""
},
{
"path": "golang/transport/doc.go",
"chars": 150,
"preview": "// Package transport provides several dialers and listeners for getting qmux sessions over TCP, Unix sockets, WebSocket,"
},
{
"path": "golang/transport/listen.go",
"chars": 302,
"preview": "package transport\n\nimport \"github.com/progrium/qmux/golang/mux\"\n\ntype Listener interface {\n\t// Close closes the listener"
},
{
"path": "golang/transport/listen_io.go",
"chars": 717,
"preview": "package transport\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang/sessio"
},
{
"path": "golang/transport/listen_net.go",
"chars": 1280,
"preview": "package transport\n\nimport (\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang/sessi"
},
{
"path": "golang/transport/listen_ws.go",
"chars": 792,
"preview": "package transport\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/progrium/qmux/golang/mux\"\n\t\"github.com/progrium/qmux/golang"
},
{
"path": "golang/transport/transport_test.go",
"chars": 2698,
"preview": "package transport\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/progrium/qmux/golang"
},
{
"path": "typescript/Makefile",
"chars": 108,
"preview": "\nbuild: dist/qmux.js\n\ndist/qmux.js: **.ts\n\tmkdir -p dist\n\tdeno bundle -c tsconfig.json index.ts dist/qmux.js"
},
{
"path": "typescript/README.md",
"chars": 21,
"preview": "# qmux for TypeScript"
},
{
"path": "typescript/api.ts",
"chars": 498,
"preview": "\nexport interface IConn {\n read(len: number): Promise<Uint8Array | undefined>;\n write(buffer: Uint8Array): Promise"
},
{
"path": "typescript/channel.ts",
"chars": 6149,
"preview": "// @ts-ignore\nimport * as util from \"./util.ts\";\n// @ts-ignore\nimport * as codec from \"./codec/index.ts\";\n// @ts-ignore\n"
},
{
"path": "typescript/codec/codec_test.ts",
"chars": 406,
"preview": "import {\n assertEquals,\n} from \"https://deno.land/std/testing/asserts.ts\";\n\n// @ts-ignore\nimport * as codec from \"./i"
},
{
"path": "typescript/codec/decoder.ts",
"chars": 3415,
"preview": "// @ts-ignore\nimport * as msg from \"./message.ts\";\n// @ts-ignore\nimport * as api from \"../api.ts\";\n// @ts-ignore\nimport "
},
{
"path": "typescript/codec/encoder.ts",
"chars": 2744,
"preview": "// @ts-ignore\nimport * as api from \"../api.ts\";\n// @ts-ignore\nimport * as msg from \"./message.ts\";\n\nexport class Encoder"
},
{
"path": "typescript/codec/index.ts",
"chars": 132,
"preview": "// @ts-ignore\nexport * from \"./message.ts\";\n// @ts-ignore\nexport * from \"./encoder.ts\";\n// @ts-ignore\nexport * from \"./d"
},
{
"path": "typescript/codec/message.ts",
"chars": 1366,
"preview": "\nexport const OpenID = 100;\nexport const OpenConfirmID = 101;\nexport const OpenFailureID = 102;\nexport const WindowAdjus"
},
{
"path": "typescript/index.ts",
"chars": 157,
"preview": "// https://github.com/Microsoft/TypeScript/issues/27481\n// @ts-ignore\nexport * from \"./internal.ts\";\n// @ts-ignore\nexpor"
},
{
"path": "typescript/internal.ts",
"chars": 88,
"preview": "// @ts-ignore\nexport * from \"./session.ts\";\n// @ts-ignore\nexport * from \"./channel.ts\";\n"
},
{
"path": "typescript/session.ts",
"chars": 4264,
"preview": "// @ts-ignore\nimport * as api from \"./api.ts\";\n// @ts-ignore\nimport * as codec from \"./codec/index.ts\";\n// @ts-ignore\nim"
},
{
"path": "typescript/session_test.ts",
"chars": 3210,
"preview": "import {\n assertEquals,\n} from \"https://deno.land/std/testing/asserts.ts\";\n\nimport * as session from \"./session.ts\";\n"
},
{
"path": "typescript/transport/deno/tcp.ts",
"chars": 1599,
"preview": "// @ts-ignore\nimport * as api from \"../../api.ts\";\n\n\nexport class Listener implements api.IConnListener {\n listener: "
},
{
"path": "typescript/transport/deno/websocket.ts",
"chars": 2332,
"preview": "import { StandardWebSocketClient, WebSocketClient, WebSocketServer } from \"https://deno.land/x/websocket@v0.1.2/mod.ts\";"
},
{
"path": "typescript/transport/websocket.ts",
"chars": 2536,
"preview": "// @ts-ignore\nimport * as api from \"./../api.ts\";\n// @ts-ignore\nimport * as internal from \"./../internal.ts\";\n// @ts-ign"
},
{
"path": "typescript/tsconfig.json",
"chars": 132,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"es2016\", \"dom\", \"es5\"],\n \"noImplicitAny\": true,\n \"allowJs\": tr"
},
{
"path": "typescript/util.ts",
"chars": 3209,
"preview": "\nexport function concat(list: Uint8Array[], totalLength: number): Uint8Array {\n let buf = new Uint8Array(totalLength)"
}
]
About this extraction
This page contains the full source code of the progrium/qmux GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (85.7 KB), approximately 24.7k tokens, and a symbol index with 236 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.