From 253ea94d2a44ee9ec86f749cf7ca9c1e70594265 Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Wed, 12 Aug 2020 11:32:39 +0100 Subject: [PATCH] Better and more explicit handling of first packet, reduces exposure of active probing --- go.mod | 2 + go.sum | 6 + internal/common/tls.go | 1 + internal/server/auth.go | 13 +- internal/server/auth_test.go | 8 +- internal/server/dispatcher.go | 111 ++++++++++++++--- internal/server/dispatcher_test.go | 192 +++++++++++++++++++++++++++++ 7 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 internal/server/dispatcher_test.go diff --git a/go.mod b/go.mod index 4bb64cd..e348b4a 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,10 @@ require ( github.com/juju/ratelimit v1.0.1 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/mitchellh/gox v1.0.1 // indirect github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 github.com/sirupsen/logrus v1.5.0 + github.com/stretchr/testify v1.2.2 go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect diff --git a/go.sum b/go.sum index 7d5b471..f69867d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= @@ -17,6 +19,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= +github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY= diff --git a/internal/common/tls.go b/internal/common/tls.go index 7b234d9..2f95905 100644 --- a/internal/common/tls.go +++ b/internal/common/tls.go @@ -74,6 +74,7 @@ func (tls *TLSConn) Read(buffer []byte) (n int, err error) { err = io.ErrShortBuffer return } + // we overwrite the record layer here return io.ReadFull(tls.Conn, buffer[:dataLength]) } diff --git a/internal/server/auth.go b/internal/server/auth.go index c1075ac..bbbbf58 100644 --- a/internal/server/auth.go +++ b/internal/server/auth.go @@ -66,18 +66,7 @@ var ErrBadProxyMethod = errors.New("invalid proxy method") // if it is from a Cloak client, it returns the ClientInfo with the decrypted fields. It doesn't check if the user // is authorised. It also returns a finisher callback function to be called when the caller wishes to proceed with // the handshake -func AuthFirstPacket(firstPacket []byte, sta *State) (info ClientInfo, finisher Responder, err error) { - var transport Transport - switch firstPacket[0] { - case 0x47: - transport = &WebSocket{} - case 0x16: - transport = &TLS{} - default: - err = ErrUnrecognisedProtocol - return - } - +func AuthFirstPacket(firstPacket []byte, transport Transport, sta *State) (info ClientInfo, finisher Responder, err error) { fragments, finisher, err := transport.processFirstPacket(firstPacket, sta.StaticPv) if err != nil { return diff --git a/internal/server/auth_test.go b/internal/server/auth_test.go index a0f7b24..66738a0 100644 --- a/internal/server/auth_test.go +++ b/internal/server/auth_test.go @@ -138,7 +138,7 @@ func TestAuthFirstPacket(t *testing.T) { t.Run("TLS correct", func(t *testing.T) { sta := getNewState() chBytes, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - info, _, err := AuthFirstPacket(chBytes, sta) + info, _, err := AuthFirstPacket(chBytes, TLS{}, sta) if err != nil { t.Errorf("failed to get client info: %v", err) return @@ -155,12 +155,12 @@ func TestAuthFirstPacket(t *testing.T) { t.Run("TLS correct but replay", func(t *testing.T) { sta := getNewState() chBytes, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - _, _, err := AuthFirstPacket(chBytes, sta) + _, _, err := AuthFirstPacket(chBytes, TLS{}, sta) if err != nil { t.Error("failed to prepare for the first time") return } - _, _, err = AuthFirstPacket(chBytes, sta) + _, _, err = AuthFirstPacket(chBytes, TLS{}, sta) if err != ErrReplay { t.Errorf("failed to return ErrReplay, got %v instead", err) return @@ -181,7 +181,7 @@ Sec-WebSocket-Version: 13 Upgrade: websocket ` - info, _, err := AuthFirstPacket([]byte(req), sta) + info, _, err := AuthFirstPacket([]byte(req), WebSocket{}, sta) if err != nil { t.Errorf("failed to get client info: %v", err) return diff --git a/internal/server/dispatcher.go b/internal/server/dispatcher.go index 86d7b0a..80c5ef9 100644 --- a/internal/server/dispatcher.go +++ b/internal/server/dispatcher.go @@ -3,6 +3,8 @@ package server import ( "bytes" "encoding/base64" + "encoding/binary" + "fmt" "github.com/cbeuw/Cloak/internal/common" "github.com/cbeuw/Cloak/internal/server/usermanager" "io" @@ -37,21 +39,91 @@ func Serve(l net.Listener, sta *State) { } } +func connReadLine(conn net.Conn, buf []byte) (int, error) { + i := 0 + for ; i < len(buf); i++ { + _, err := io.ReadFull(conn, buf[i:i+1]) + if err != nil { + return i, err + } + if buf[i] == '\n' { + return i + 1, nil + } + } + return i, io.ErrShortBuffer +} + +func readFirstPacket(conn net.Conn, buf []byte, timeout time.Duration) (int, Transport, bool, error) { + conn.SetReadDeadline(time.Now().Add(timeout)) + defer conn.SetReadDeadline(time.Time{}) + + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + err = fmt.Errorf("read error after connection is established: %v", err) + conn.Close() + return 0, nil, false, err + } + + // TODO: give the option to match the protocol with port + bufOffset := 1 + var transport Transport + switch buf[0] { + case 0x16: + transport = TLS{} + recordLayerLength := 5 + + i, err := io.ReadFull(conn, buf[bufOffset:recordLayerLength]) + bufOffset += i + if err != nil { + err = fmt.Errorf("read error after connection is established: %v", err) + conn.Close() + return bufOffset, transport, false, err + } + dataLength := int(binary.BigEndian.Uint16(buf[3:5])) + if dataLength+recordLayerLength > len(buf) { + return bufOffset, transport, true, io.ErrShortBuffer + } + + i, err = io.ReadFull(conn, buf[recordLayerLength:dataLength+recordLayerLength]) + bufOffset += i + if err != nil { + err = fmt.Errorf("read error after connection is established: %v", err) + conn.Close() + return bufOffset, transport, false, err + } + case 0x47: + transport = WebSocket{} + + for { + i, err := connReadLine(conn, buf[bufOffset:]) + line := buf[bufOffset : bufOffset+i] + bufOffset += i + if err != nil { + if err == io.ErrShortBuffer { + return bufOffset, transport, true, err + } else { + err = fmt.Errorf("error reading first packet: %v", err) + conn.Close() + return bufOffset, transport, false, err + } + } + + if bytes.Equal(line, []byte("\r\n")) { + break + } + } + default: + err = fmt.Errorf("unrecognised protocol signature") + return bufOffset, transport, true, ErrUnrecognisedProtocol + } + return bufOffset, transport, true, nil +} + func dispatchConnection(conn net.Conn, sta *State) { - remoteAddr := conn.RemoteAddr() var err error buf := make([]byte, 1500) - // TODO: potential fingerprint for active probers here - conn.SetReadDeadline(time.Now().Add(3 * time.Second)) - i, err := io.ReadAtLeast(conn, buf, 1) - if err != nil { - log.WithField("remoteAddr", remoteAddr). - Infof("failed to read anything after connection is established: %v", err) - conn.Close() - return - } - conn.SetReadDeadline(time.Time{}) + i, transport, redirOnErr, err := readFirstPacket(conn, buf, 15*time.Second) data := buf[:i] goWeb := func() { @@ -73,10 +145,21 @@ func dispatchConnection(conn net.Conn, sta *State) { go io.Copy(conn, webConn) } - ci, finishHandshake, err := AuthFirstPacket(data, sta) + if err != nil { + log.WithField("remoteAddr", conn.RemoteAddr()). + Warnf("error reading first packet: %v", err) + if redirOnErr { + goWeb() + } else { + conn.Close() + } + return + } + + ci, finishHandshake, err := AuthFirstPacket(data, transport, sta) if err != nil { log.WithFields(log.Fields{ - "remoteAddr": remoteAddr, + "remoteAddr": conn.RemoteAddr(), "UID": b64(ci.UID), "sessionId": ci.SessionId, "proxyMethod": ci.ProxyMethod, @@ -132,7 +215,7 @@ func dispatchConnection(conn net.Conn, sta *State) { if err != nil { log.WithFields(log.Fields{ "UID": b64(ci.UID), - "remoteAddr": remoteAddr, + "remoteAddr": conn.RemoteAddr(), "error": err, }).Warn("+1 unauthorised UID") goWeb() diff --git a/internal/server/dispatcher_test.go b/internal/server/dispatcher_test.go new file mode 100644 index 0000000..a4b9c7f --- /dev/null +++ b/internal/server/dispatcher_test.go @@ -0,0 +1,192 @@ +package server + +import ( + "encoding/hex" + "github.com/cbeuw/connutil" + "github.com/stretchr/testify/assert" + "io" + "net" + "testing" + "time" +) + +type rfpReturnValue struct { + n int + transport Transport + redirOnErr bool + err error +} + +const timeout = 500 * time.Millisecond + +func TestReadFirstPacket(t *testing.T) { + rfp := func(conn net.Conn, buf []byte, retChan chan<- rfpReturnValue) { + ret := rfpReturnValue{} + ret.n, ret.transport, ret.redirOnErr, ret.err = readFirstPacket(conn, buf, timeout) + retChan <- ret + } + + t.Run("Good TLS", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + local.Write(first) + + ret := <-retChan + + assert.Equal(t, len(first), ret.n) + assert.Equal(t, first, buf[:ret.n]) + assert.IsType(t, TLS{}, ret.transport) + assert.NoError(t, ret.err) + }) + + t.Run("Good TLS but buf too small", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 10) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + local.Write(first) + + ret := <-retChan + + assert.Equal(t, io.ErrShortBuffer, ret.err) + assert.True(t, ret.redirOnErr) + assert.Equal(t, first[:ret.n], buf[:ret.n]) + + }) + + t.Run("Incomplete timeout", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + first, _ := hex.DecodeString("160301") + local.Write(first) + select { + case ret := <-retChan: + assert.Equal(t, len(first), ret.n) + assert.False(t, ret.redirOnErr) + assert.Error(t, ret.err) + case <-time.After(2 * timeout): + assert.Fail(t, "readFirstPacket should have timed out") + } + }) + + t.Run("Incomplete payload timeout", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + first, _ := hex.DecodeString("16030101010000") + local.Write(first) + select { + case ret := <-retChan: + assert.Equal(t, len(first), ret.n) + assert.False(t, ret.redirOnErr) + assert.Error(t, ret.err) + case <-time.After(2 * timeout): + assert.Fail(t, "readFirstPacket should have timed out") + } + }) + + t.Run("Good TLS staggered", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + first, _ := hex.DecodeString("1603010200010001fc0303ac530b5778469dbbc3f9a83c6ac35b63aa6a70c2014026ade30f2faf0266f0242068424f320bcad49b4315a761f9f6dec32b0a403c2d8c0ab337608a694c6e411c0024130113031302c02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100018f00000011000f00000c7777772e62696e672e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000033006b0069001d00204655c2c83aaed1db2e89ed17d671fcdc76dc96e36bde8840022f1bda2f31019600170041543af1f8d28b37d984073f40e8361613da502f16e4039f00656f427de0f66480b2e77e3e552e126bb0cc097168f6e5454c7f9501126a2377fb40151f6cfc007e0e002b0009080304030303020301000d0018001604030503060308040805080604010501060102030201002d00020101001c00024001001500920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + local.Write(first[:100]) + time.Sleep(timeout / 2) + local.Write(first[100:]) + + ret := <-retChan + + assert.Equal(t, len(first), ret.n) + assert.Equal(t, first, buf[:ret.n]) + assert.IsType(t, TLS{}, ret.transport) + assert.NoError(t, ret.err) + }) + + t.Run("Good WebSocket", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n" + req := []byte(reqStr) + local.Write(req) + + ret := <-retChan + + assert.Equal(t, len(req), ret.n) + assert.Equal(t, req, buf[:ret.n]) + assert.IsType(t, WebSocket{}, ret.transport) + assert.NoError(t, ret.err) + }) + + t.Run("Good WebSocket but buf too small", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 10) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n" + req := []byte(reqStr) + local.Write(req) + + ret := <-retChan + + assert.Equal(t, io.ErrShortBuffer, ret.err) + assert.True(t, ret.redirOnErr) + assert.Equal(t, req[:ret.n], buf[:ret.n]) + }) + + t.Run("Incomplete WebSocket timeout", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + reqStr := "GET /" + req := []byte(reqStr) + local.Write(req) + + select { + case ret := <-retChan: + assert.Equal(t, len(req), ret.n) + assert.False(t, ret.redirOnErr) + assert.Error(t, ret.err) + case <-time.After(2 * timeout): + assert.Fail(t, "readFirstPacket should have timed out") + } + }) + + t.Run("Staggered WebSocket", func(t *testing.T) { + local, remote := connutil.AsyncPipe() + buf := make([]byte, 1500) + retChan := make(chan rfpReturnValue) + go rfp(remote, buf, retChan) + + reqStr := "GET / HTTP/1.1\r\nHost: d2jkinvisak5y9.cloudfront.net:443\r\nUser-Agent: Go-http-client/1.1\r\nConnection: Upgrade\r\nHidden: oJxeEwfDWg5k5Jbl8ttZD1sc0fHp8VjEtXHsqEoSrnaLRe/M+KGXkOzpc/2fRRg9Vk+wIWRsfv8IpoBPLbqO+ZfGsPXTjUJGiI9BqxrcJfkxncXA7FAHGpTc84tzBtZZ\r\nSec-WebSocket-Key: lJYh7X8DRXW1U0h9WKwVMA==\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n\r\n" + req := []byte(reqStr) + local.Write(req[:100]) + time.Sleep(timeout / 2) + local.Write(req[100:]) + + ret := <-retChan + + assert.Equal(t, len(req), ret.n) + assert.Equal(t, req, buf[:ret.n]) + assert.IsType(t, WebSocket{}, ret.transport) + assert.NoError(t, ret.err) + }) +}