mirror of
https://github.com/soheilhy/cmux.git
synced 2026-08-29 06:44:32 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b740c1fa6 | ||
|
|
5ec6847320 | ||
|
|
cdd3331e3e | ||
|
|
ce11cfdf3a | ||
|
|
ae889c5259 | ||
|
|
3c0ee784ab | ||
|
|
a192073df5 | ||
|
|
e13d1cbf02 | ||
|
|
8a8ea3c539 | ||
|
|
f7603f4e1c | ||
|
|
e09e9389d8 | ||
|
|
444ce56efe | ||
|
|
cfc68f9888 | ||
|
|
e96bd75f84 | ||
|
|
be5b383fd5 | ||
|
|
b9e684ba4e | ||
|
|
bb79a83465 | ||
|
|
7e08502c7a | ||
|
|
0c129dc694 | ||
|
|
34a8ab6cda | ||
|
|
3b204bab2a | ||
|
|
9a3402ad7a | ||
|
|
4f90533583 | ||
|
|
8cd60510aa | ||
|
|
f671b41193 | ||
|
|
885b8d8a14 | ||
|
|
0068a46c9c |
50
.github/workflows/ci.yml
vendored
Normal file
50
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
library:
|
||||
name: library (go ${{ matrix.go }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.23", "1.24"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: gofmt
|
||||
run: test -z "$(gofmt -s -l .)" || (gofmt -s -l . && exit 1)
|
||||
- name: vet
|
||||
run: go vet ./...
|
||||
- name: build
|
||||
run: go build ./...
|
||||
- name: test
|
||||
run: go test -race ./...
|
||||
|
||||
example:
|
||||
name: example
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: example
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: gofmt
|
||||
run: test -z "$(gofmt -s -l .)" || (gofmt -s -l . && exit 1)
|
||||
- name: vet
|
||||
run: go vet ./...
|
||||
- name: build
|
||||
run: go build ./...
|
||||
- name: test
|
||||
run: go test -race ./...
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,29 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
gobuild_args: -race
|
||||
|
||||
before_install:
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then go get -u github.com/kisielk/errcheck; fi
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then go get -u github.com/golang/lint/golint; fi
|
||||
|
||||
before_script:
|
||||
- '! gofmt -s -l . | read'
|
||||
- echo $TRAVIS_GO_VERSION
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then golint ./...; fi
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then errcheck ./...; fi
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then go tool vet .; fi
|
||||
- if [[ $TRAVIS_GO_VERSION == 1.6* ]]; then go tool vet --shadow .; fi
|
||||
|
||||
script:
|
||||
- go test -bench . -v ./...
|
||||
- go test -race -bench . -v ./...
|
||||
@@ -25,14 +25,14 @@ trpcL := m.Match(cmux.Any()) // Any means anything that is not yet matched.
|
||||
|
||||
// Create your protocol servers.
|
||||
grpcS := grpc.NewServer()
|
||||
grpchello.RegisterGreeterServer(grpcs, &server{})
|
||||
grpchello.RegisterGreeterServer(grpcS, &server{})
|
||||
|
||||
httpS := &http.Server{
|
||||
Handler: &helloHTTP1Handler{},
|
||||
}
|
||||
|
||||
trpcS := rpc.NewServer()
|
||||
s.Register(&ExampleRPCRcvr{})
|
||||
trpcS.Register(&ExampleRPCRcvr{})
|
||||
|
||||
// Use the muxed listeners for your servers.
|
||||
go grpcS.Serve(grpcL)
|
||||
|
||||
52
cmux.go
52
cmux.go
@@ -15,6 +15,7 @@
|
||||
package cmux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -61,6 +62,9 @@ func (e errListenerClosed) Timeout() bool { return false }
|
||||
// listener is closed.
|
||||
var ErrListenerClosed = errListenerClosed("mux: listener closed")
|
||||
|
||||
// ErrServerClosed is returned from muxListener.Accept when mux server is closed.
|
||||
var ErrServerClosed = errors.New("mux: server closed")
|
||||
|
||||
// for readability of readTimeout
|
||||
var noTimeout time.Duration
|
||||
|
||||
@@ -93,6 +97,8 @@ type CMux interface {
|
||||
// Serve starts multiplexing the listener. Serve blocks and perhaps
|
||||
// should be invoked concurrently within a go routine.
|
||||
Serve() error
|
||||
// Closes cmux server and stops accepting any connections on listener
|
||||
Close()
|
||||
// HandleError registers an error handler that handles listener errors.
|
||||
HandleError(ErrorHandler)
|
||||
// sets a timeout for the read of matchers
|
||||
@@ -108,16 +114,18 @@ type cMux struct {
|
||||
root net.Listener
|
||||
bufLen int
|
||||
errh ErrorHandler
|
||||
donec chan struct{}
|
||||
sls []matchersListener
|
||||
readTimeout time.Duration
|
||||
donec chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func matchersToMatchWriters(matchers []Matcher) []MatchWriter {
|
||||
mws := make([]MatchWriter, 0, len(matchers))
|
||||
for _, m := range matchers {
|
||||
cm := m
|
||||
mws = append(mws, func(w io.Writer, r io.Reader) bool {
|
||||
return m(r)
|
||||
return cm(r)
|
||||
})
|
||||
}
|
||||
return mws
|
||||
@@ -132,6 +140,7 @@ func (m *cMux) MatchWithWriters(matchers ...MatchWriter) net.Listener {
|
||||
ml := muxListener{
|
||||
Listener: m.root,
|
||||
connc: make(chan net.Conn, m.bufLen),
|
||||
donec: make(chan struct{}),
|
||||
}
|
||||
m.sls = append(m.sls, matchersListener{ss: matchers, l: ml})
|
||||
return ml
|
||||
@@ -145,7 +154,7 @@ func (m *cMux) Serve() error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
defer func() {
|
||||
close(m.donec)
|
||||
m.closeDoneChans()
|
||||
wg.Wait()
|
||||
|
||||
for _, sl := range m.sls {
|
||||
@@ -203,6 +212,30 @@ func (m *cMux) serve(c net.Conn, donec <-chan struct{}, wg *sync.WaitGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cMux) Close() {
|
||||
m.closeDoneChans()
|
||||
}
|
||||
|
||||
func (m *cMux) closeDoneChans() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-m.donec:
|
||||
// Already closed. Don't close again
|
||||
default:
|
||||
close(m.donec)
|
||||
}
|
||||
for _, sl := range m.sls {
|
||||
select {
|
||||
case <-sl.l.donec:
|
||||
// Already closed. Don't close again
|
||||
default:
|
||||
close(sl.l.donec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cMux) HandleError(h ErrorHandler) {
|
||||
m.errh = h
|
||||
}
|
||||
@@ -222,14 +255,19 @@ func (m *cMux) handleErr(err error) bool {
|
||||
type muxListener struct {
|
||||
net.Listener
|
||||
connc chan net.Conn
|
||||
donec chan struct{}
|
||||
}
|
||||
|
||||
func (l muxListener) Accept() (net.Conn, error) {
|
||||
c, ok := <-l.connc
|
||||
if !ok {
|
||||
return nil, ErrListenerClosed
|
||||
select {
|
||||
case c, ok := <-l.connc:
|
||||
if !ok {
|
||||
return nil, ErrListenerClosed
|
||||
}
|
||||
return c, nil
|
||||
case <-l.donec:
|
||||
return nil, ErrServerClosed
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// MuxConn wraps a net.Conn and provides transparent sniffing of connection data.
|
||||
|
||||
212
cmux_test.go
212
cmux_test.go
@@ -16,14 +16,18 @@ package cmux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -76,7 +80,7 @@ func (l *chanListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
|
||||
func testListener(t *testing.T) (net.Listener, func()) {
|
||||
l, err := net.Listen("tcp4", ":0")
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -123,13 +127,62 @@ func runTestHTTPServer(errCh chan<- error, l net.Listener) {
|
||||
mu.Unlock()
|
||||
},
|
||||
}
|
||||
if err := s.Serve(l); err != ErrListenerClosed {
|
||||
if err := s.Serve(l); err != ErrListenerClosed && err != ErrServerClosed {
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
|
||||
func generateTLSCert(t *testing.T) {
|
||||
err := exec.Command("go", "run", build.Default.GOROOT+"/src/crypto/tls/generate_cert.go", "--host", "*").Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupTLSCert(t *testing.T) {
|
||||
err := os.Remove("cert.pem")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = os.Remove("key.pem")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func runTestTLSServer(errCh chan<- error, l net.Listener) {
|
||||
certificate, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
log.Printf("1")
|
||||
return
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
Rand: rand.Reader,
|
||||
}
|
||||
|
||||
tlsl := tls.NewListener(l, config)
|
||||
runTestHTTPServer(errCh, tlsl)
|
||||
}
|
||||
|
||||
func runTestHTTP1Client(t *testing.T, addr net.Addr) {
|
||||
r, err := http.Get("http://" + addr.String())
|
||||
runTestHTTPClient(t, "http", addr)
|
||||
}
|
||||
|
||||
func runTestTLSClient(t *testing.T, addr net.Addr) {
|
||||
runTestHTTPClient(t, "https", addr)
|
||||
}
|
||||
|
||||
func runTestHTTPClient(t *testing.T, proto string, addr net.Addr) {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
r, err := client.Get(proto + "://" + addr.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -140,7 +193,7 @@ func runTestHTTP1Client(t *testing.T, addr net.Addr) {
|
||||
}
|
||||
}()
|
||||
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -164,7 +217,7 @@ func runTestRPCServer(errCh chan<- error, l net.Listener) {
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if err != ErrListenerClosed {
|
||||
if err != ErrListenerClosed && err != ErrServerClosed {
|
||||
errCh <- err
|
||||
}
|
||||
return
|
||||
@@ -199,7 +252,7 @@ func TestTimeout(t *testing.T) {
|
||||
lis, Close := testListener(t)
|
||||
defer Close()
|
||||
result := make(chan int, 5)
|
||||
testDuration := time.Millisecond * 100
|
||||
testDuration := time.Millisecond * 500
|
||||
m := New(lis)
|
||||
m.SetReadTimeout(testDuration)
|
||||
http1 := m.Match(HTTP1Fast())
|
||||
@@ -267,7 +320,7 @@ func TestTimeout(t *testing.T) {
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -281,10 +334,11 @@ func TestRead(t *testing.T) {
|
||||
writer, reader := net.Pipe()
|
||||
go func() {
|
||||
if _, err := io.WriteString(writer, strings.Repeat(payload, mult)); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -325,7 +379,7 @@ func TestRead(t *testing.T) {
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -345,9 +399,36 @@ func TestAny(t *testing.T) {
|
||||
runTestHTTP1Client(t, l.Addr())
|
||||
}
|
||||
|
||||
func TestTLS(t *testing.T) {
|
||||
generateTLSCert(t)
|
||||
defer cleanupTLSCert(t)
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatal(err)
|
||||
default:
|
||||
}
|
||||
}()
|
||||
l, cleanup := testListener(t)
|
||||
defer cleanup()
|
||||
|
||||
muxl := New(l)
|
||||
tlsl := muxl.Match(TLS())
|
||||
httpl := muxl.Match(Any())
|
||||
|
||||
go runTestTLSServer(errCh, tlsl)
|
||||
go runTestHTTPServer(errCh, httpl)
|
||||
go safeServe(errCh, muxl)
|
||||
|
||||
runTestHTTP1Client(t, l.Addr())
|
||||
runTestTLSClient(t, l.Addr())
|
||||
}
|
||||
|
||||
func TestHTTP2(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -358,10 +439,11 @@ func TestHTTP2(t *testing.T) {
|
||||
writer, reader := net.Pipe()
|
||||
go func() {
|
||||
if _, err := io.WriteString(writer, http2.ClientPreface); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -397,8 +479,22 @@ func TestHTTP2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHTTP2MatchHeaderField(t *testing.T) {
|
||||
testHTTP2MatchHeaderField(t, HTTP2HeaderField, "value", "value", "anothervalue")
|
||||
}
|
||||
|
||||
func TestHTTP2MatchHeaderFieldPrefix(t *testing.T) {
|
||||
testHTTP2MatchHeaderField(t, HTTP2HeaderFieldPrefix, "application/grpc+proto", "application/grpc", "application/json")
|
||||
}
|
||||
|
||||
func testHTTP2MatchHeaderField(
|
||||
t *testing.T,
|
||||
matcherConstructor func(string, string) Matcher,
|
||||
headerValue string,
|
||||
matchValue string,
|
||||
notMatchValue string,
|
||||
) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -407,16 +503,17 @@ func TestHTTP2MatchHeaderField(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
name := "name"
|
||||
value := "value"
|
||||
writer, reader := net.Pipe()
|
||||
go func() {
|
||||
if _, err := io.WriteString(writer, http2.ClientPreface); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := hpack.NewEncoder(&buf)
|
||||
if err := enc.WriteField(hpack.HeaderField{Name: name, Value: value}); err != nil {
|
||||
t.Fatal(err)
|
||||
if err := enc.WriteField(hpack.HeaderField{Name: name, Value: headerValue}); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
framer := http2.NewFramer(writer, nil)
|
||||
err := framer.WriteHeaders(http2.HeadersFrameParam{
|
||||
@@ -426,10 +523,11 @@ func TestHTTP2MatchHeaderField(t *testing.T) {
|
||||
EndHeaders: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -443,9 +541,9 @@ func TestHTTP2MatchHeaderField(t *testing.T) {
|
||||
return false
|
||||
})
|
||||
// Create a matcher that cannot match the response.
|
||||
muxl.Match(HTTP2HeaderField(name, "another"+value))
|
||||
muxl.Match(matcherConstructor(name, notMatchValue))
|
||||
// Then match with the expected field.
|
||||
h2l := muxl.Match(HTTP2HeaderField(name, value))
|
||||
h2l := muxl.Match(matcherConstructor(name, matchValue))
|
||||
go safeServe(errCh, muxl)
|
||||
muxedConn, err := h2l.Accept()
|
||||
close(l.connCh)
|
||||
@@ -464,7 +562,7 @@ func TestHTTP2MatchHeaderField(t *testing.T) {
|
||||
|
||||
func TestHTTPGoRPC(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -489,7 +587,7 @@ func TestHTTPGoRPC(t *testing.T) {
|
||||
|
||||
func TestErrorHandler(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -528,9 +626,38 @@ func TestErrorHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
func TestMultipleMatchers(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatal(err)
|
||||
default:
|
||||
}
|
||||
}()
|
||||
l, cleanup := testListener(t)
|
||||
defer cleanup()
|
||||
|
||||
matcher := func(r io.Reader) bool {
|
||||
return true
|
||||
}
|
||||
unmatcher := func(r io.Reader) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
muxl := New(l)
|
||||
lis := muxl.Match(unmatcher, matcher, unmatcher)
|
||||
|
||||
go runTestHTTPServer(errCh, lis)
|
||||
go safeServe(errCh, muxl)
|
||||
|
||||
runTestHTTP1Client(t, l.Addr())
|
||||
}
|
||||
|
||||
func TestListenerClose(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -562,15 +689,42 @@ func TestClose(t *testing.T) {
|
||||
|
||||
// Second connection either goes through or it is closed.
|
||||
if _, err := anyl.Accept(); err != nil {
|
||||
if err != ErrListenerClosed {
|
||||
if err != ErrListenerClosed && err != ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c2.Read([]byte{}); err != io.ErrClosedPipe {
|
||||
// The error is either io.ErrClosedPipe or net.OpError wrapping
|
||||
// a net.pipeError depending on the go version.
|
||||
if _, err := c2.Read([]byte{}); !strings.Contains(err.Error(), "closed") {
|
||||
t.Fatalf("connection is not closed and is leaked: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
defer leakCheck(t)()
|
||||
errCh := make(chan error, 1)
|
||||
defer func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatal(err)
|
||||
default:
|
||||
}
|
||||
}()
|
||||
l, cleanup := testListener(t)
|
||||
defer cleanup()
|
||||
|
||||
muxl := New(l)
|
||||
anyl := muxl.Match(Any())
|
||||
|
||||
go safeServe(errCh, muxl)
|
||||
|
||||
muxl.Close()
|
||||
|
||||
if _, err := anyl.Accept(); err != ErrServerClosed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Cribbed from google.golang.org/grpc/test/end2end_test.go.
|
||||
|
||||
// interestingGoroutines returns all goroutines we care about for the purpose
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package cmux_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -25,10 +26,10 @@ import (
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/soheilhy/cmux"
|
||||
"google.golang.org/grpc/examples/helloworld/helloworld"
|
||||
grpchello "google.golang.org/grpc/examples/helloworld/helloworld"
|
||||
)
|
||||
|
||||
@@ -86,7 +87,9 @@ func serveRPC(l net.Listener) {
|
||||
}
|
||||
}
|
||||
|
||||
type grpcServer struct{}
|
||||
type grpcServer struct {
|
||||
helloworld.UnimplementedGreeterServer
|
||||
}
|
||||
|
||||
func (s *grpcServer) SayHello(ctx context.Context, in *grpchello.HelloRequest) (
|
||||
*grpchello.HelloReply, error) {
|
||||
@@ -112,7 +115,7 @@ func Example() {
|
||||
|
||||
// We first match the connection against HTTP2 fields. If matched, the
|
||||
// connection will be sent through the "grpcl" listener.
|
||||
grpcl := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
|
||||
grpcl := m.Match(cmux.HTTP2HeaderFieldPrefix("content-type", "application/grpc"))
|
||||
//Otherwise, we match it againts a websocket upgrade request.
|
||||
wsl := m.Match(cmux.HTTP1HeaderField("Upgrade", "websocket"))
|
||||
|
||||
19
example/go.mod
Normal file
19
example/go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module github.com/soheilhy/cmux/example
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/soheilhy/cmux v0.0.0-00010101000000-000000000000
|
||||
golang.org/x/net v0.53.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/grpc/examples v0.0.0-20260605180800-0f3086db7a75
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
replace github.com/soheilhy/cmux => ../
|
||||
40
example/go.sum
Normal file
40
example/go.sum
Normal file
@@ -0,0 +1,40 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc/examples v0.0.0-20260605180800-0f3086db7a75 h1:AEIy9rWQCaqGLD5thytE2ox41wjT4vOe43/yh2enp+A=
|
||||
google.golang.org/grpc/examples v0.0.0-20260605180800-0f3086db7a75/go.mod h1:Cj6Qdy58KTzw400M0aa1PA3rNLrhQgPFdqXxShtOXmM=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/soheilhy/cmux
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require golang.org/x/net v0.42.0
|
||||
|
||||
require golang.org/x/text v0.27.0 // indirect
|
||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
89
matchers.go
89
matchers.go
@@ -16,8 +16,8 @@ package cmux
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -37,6 +37,11 @@ func PrefixMatcher(strs ...string) Matcher {
|
||||
return pt.matchPrefix
|
||||
}
|
||||
|
||||
func prefixByteMatcher(list ...[]byte) Matcher {
|
||||
pt := newPatriciaTree(list...)
|
||||
return pt.matchPrefix
|
||||
}
|
||||
|
||||
var defaultHTTPMethods = []string{
|
||||
"OPTIONS",
|
||||
"GET",
|
||||
@@ -57,6 +62,28 @@ func HTTP1Fast(extMethods ...string) Matcher {
|
||||
return PrefixMatcher(append(defaultHTTPMethods, extMethods...)...)
|
||||
}
|
||||
|
||||
// TLS matches HTTPS requests.
|
||||
//
|
||||
// By default, any TLS handshake packet is matched. An optional whitelist
|
||||
// of versions can be passed in to restrict the matcher, for example:
|
||||
//
|
||||
// TLS(tls.VersionTLS11, tls.VersionTLS12)
|
||||
func TLS(versions ...int) Matcher {
|
||||
if len(versions) == 0 {
|
||||
versions = []int{
|
||||
tls.VersionSSL30,
|
||||
tls.VersionTLS10,
|
||||
tls.VersionTLS11,
|
||||
tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
prefixes := [][]byte{}
|
||||
for _, v := range versions {
|
||||
prefixes = append(prefixes, []byte{22, byte(v >> 8 & 0xff), byte(v & 0xff)})
|
||||
}
|
||||
return prefixByteMatcher(prefixes...)
|
||||
}
|
||||
|
||||
const maxHTTPRead = 4096
|
||||
|
||||
// HTTP1 parses the first line or upto 4096 bytes of the request to see if
|
||||
@@ -100,15 +127,41 @@ func HTTP2() Matcher {
|
||||
// request of an HTTP 1 connection.
|
||||
func HTTP1HeaderField(name, value string) Matcher {
|
||||
return func(r io.Reader) bool {
|
||||
return matchHTTP1Field(r, name, value)
|
||||
return matchHTTP1Field(r, name, func(gotValue string) bool {
|
||||
return gotValue == value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP2HeaderField resturns a matcher matching the header fields of the first
|
||||
// HTTP1HeaderFieldPrefix returns a matcher matching the header fields of the
|
||||
// first request of an HTTP 1 connection. If the header with key name has a
|
||||
// value prefixed with valuePrefix, this will match.
|
||||
func HTTP1HeaderFieldPrefix(name, valuePrefix string) Matcher {
|
||||
return func(r io.Reader) bool {
|
||||
return matchHTTP1Field(r, name, func(gotValue string) bool {
|
||||
return strings.HasPrefix(gotValue, valuePrefix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP2HeaderField returns a matcher matching the header fields of the first
|
||||
// headers frame.
|
||||
func HTTP2HeaderField(name, value string) Matcher {
|
||||
return func(r io.Reader) bool {
|
||||
return matchHTTP2Field(ioutil.Discard, r, name, value)
|
||||
return matchHTTP2Field(io.Discard, r, name, func(gotValue string) bool {
|
||||
return gotValue == value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP2HeaderFieldPrefix returns a matcher matching the header fields of the
|
||||
// first headers frame. If the header with key name has a value prefixed with
|
||||
// valuePrefix, this will match.
|
||||
func HTTP2HeaderFieldPrefix(name, valuePrefix string) Matcher {
|
||||
return func(r io.Reader) bool {
|
||||
return matchHTTP2Field(io.Discard, r, name, func(gotValue string) bool {
|
||||
return strings.HasPrefix(gotValue, valuePrefix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +170,20 @@ func HTTP2HeaderField(name, value string) Matcher {
|
||||
// does not block on receiving a SETTING frame.
|
||||
func HTTP2MatchHeaderFieldSendSettings(name, value string) MatchWriter {
|
||||
return func(w io.Writer, r io.Reader) bool {
|
||||
return matchHTTP2Field(w, r, name, value)
|
||||
return matchHTTP2Field(w, r, name, func(gotValue string) bool {
|
||||
return gotValue == value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP2MatchHeaderFieldPrefixSendSettings matches the header field prefix
|
||||
// and writes the settings to the server. Prefer HTTP2HeaderFieldPrefix over
|
||||
// this one, if the client does not block on receiving a SETTING frame.
|
||||
func HTTP2MatchHeaderFieldPrefixSendSettings(name, valuePrefix string) MatchWriter {
|
||||
return func(w io.Writer, r io.Reader) bool {
|
||||
return matchHTTP2Field(w, r, name, func(gotValue string) bool {
|
||||
return strings.HasPrefix(gotValue, valuePrefix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,16 +208,16 @@ func hasHTTP2Preface(r io.Reader) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func matchHTTP1Field(r io.Reader, name, value string) (matched bool) {
|
||||
func matchHTTP1Field(r io.Reader, name string, matches func(string) bool) (matched bool) {
|
||||
req, err := http.ReadRequest(bufio.NewReader(r))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return req.Header.Get(name) == value
|
||||
return matches(req.Header.Get(name))
|
||||
}
|
||||
|
||||
func matchHTTP2Field(w io.Writer, r io.Reader, name, value string) (matched bool) {
|
||||
func matchHTTP2Field(w io.Writer, r io.Reader, name string, matches func(string) bool) (matched bool) {
|
||||
if !hasHTTP2Preface(r) {
|
||||
return false
|
||||
}
|
||||
@@ -161,7 +227,7 @@ func matchHTTP2Field(w io.Writer, r io.Reader, name, value string) (matched bool
|
||||
hdec := hpack.NewDecoder(uint32(4<<10), func(hf hpack.HeaderField) {
|
||||
if hf.Name == name {
|
||||
done = true
|
||||
if hf.Value == value {
|
||||
if matches(hf.Value) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
@@ -174,6 +240,11 @@ func matchHTTP2Field(w io.Writer, r io.Reader, name, value string) (matched bool
|
||||
|
||||
switch f := f.(type) {
|
||||
case *http2.SettingsFrame:
|
||||
// Sender acknoweldged the SETTINGS frame. No need to write
|
||||
// SETTINGS again.
|
||||
if f.IsAck() {
|
||||
break
|
||||
}
|
||||
if err := framer.WriteSettings(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user