blob: a0cae3413c5389363ad7a83235ebea8e6e036514 [file]
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"fmt"
"io"
"sync"
"testing"
)
func muxPair() (*mux, *mux) {
a, b := memPipe()
s := newMux(a)
c := newMux(b)
return s, c
}
// Returns both ends of a channel, and the mux for the 2nd
// channel.
func channelPair(t *testing.T) (*channel, *channel, *mux) {
c, s := muxPair()
res := make(chan *channel, 1)
go func() {
newCh, ok := <-s.incomingChannels
if !ok {
t.Error("no incoming channel")
close(res)
return
}
if newCh.ChannelType() != "chan" {
t.Errorf("got type %q want chan", newCh.ChannelType())
newCh.Reject(Prohibited, fmt.Sprintf("got type %q want chan", newCh.ChannelType()))
close(res)
return
}
ch, _, err := newCh.Accept()
if err != nil {
t.Errorf("accept: %v", err)
close(res)
return
}
res <- ch.(*channel)
}()
ch, err := c.openChannel("chan", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
w := <-res
if w == nil {
t.Fatal("unable to get write channel")
}
return w, ch, c
}
// Test that stderr and stdout can be addressed from different
// goroutines. This is intended for use with the race detector.
func TestMuxChannelExtendedThreadSafety(t *testing.T) {
writer, reader, mux := channelPair(t)
defer writer.Close()
defer reader.Close()
defer mux.Close()
var wr, rd sync.WaitGroup
magic := "hello world"
wr.Add(2)
go func() {
io.WriteString(writer, magic)
wr.Done()
}()
go func() {
io.WriteString(writer.Stderr(), magic)
wr.Done()
}()
rd.Add(2)
go func() {
c, err := io.ReadAll(reader)
if string(c) != magic {
t.Errorf("stdout read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
go func() {
c, err := io.ReadAll(reader.Stderr())
if string(c) != magic {
t.Errorf("stderr read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
wr.Wait()
writer.CloseWrite()
rd.Wait()
}
func TestMuxReadWrite(t *testing.T) {
s, c, mux := channelPair(t)
defer s.Close()
defer c.Close()
defer mux.Close()
magic := "hello world"
magicExt := "hello stderr"
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
_, err := s.Write([]byte(magic))
if err != nil {
t.Errorf("Write: %v", err)
return
}
_, err = s.Extended(1).Write([]byte(magicExt))
if err != nil {
t.Errorf("Write: %v", err)
return
}
}()
var buf [1024]byte
n, err := c.Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got := string(buf[:n])
if got != magic {
t.Fatalf("server: got %q want %q", got, magic)
}
n, err = c.Extended(1).Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got = string(buf[:n])
if got != magicExt {
t.Fatalf("server: got %q want %q", got, magic)
}
}
func TestMuxChannelOverflow(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
writer.Write(make([]byte, 1))
}()
writer.remoteWin.waitWriterBlocked()
// Send 1 byte.
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], writer.remoteId)
marshalUint32(packet[5:], uint32(1))
packet[9] = 42
if err := writer.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
if _, err := reader.SendRequest("hello", true, nil); err == nil {
t.Errorf("SendRequest succeeded.")
}
}
func TestMuxChannelReadUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != nil {
t.Errorf("Write: %v", err)
}
writer.Close()
}()
writer.remoteWin.waitWriterBlocked()
buf := make([]byte, 32768)
for {
_, err := reader.Read(buf)
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("Read: %v", err)
}
}
}
func TestMuxChannelCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
}()
writer.remoteWin.waitWriterBlocked()
reader.Close()
}
func TestMuxConnectionCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
}()
writer.remoteWin.waitWriterBlocked()
mux.Close()
}
func TestMuxReject(t *testing.T) {
client, server := muxPair()
defer server.Close()
defer client.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
ch, ok := <-server.incomingChannels
if !ok {
t.Error("cannot accept channel")
return
}
if ch.ChannelType() != "ch" || string(ch.ExtraData()) != "extra" {
t.Errorf("unexpected channel: %q, %q", ch.ChannelType(), ch.ExtraData())
ch.Reject(RejectionReason(UnknownChannelType), UnknownChannelType.String())
return
}
ch.Reject(RejectionReason(42), "message")
}()
ch, err := client.openChannel("ch", []byte("extra"))
if ch != nil {
t.Fatal("openChannel not rejected")
}
ocf, ok := err.(*OpenChannelError)
if !ok {
t.Errorf("got %#v want *OpenChannelError", err)
} else if ocf.Reason != 42 || ocf.Message != "message" {
t.Errorf("got %#v, want {Reason: 42, Message: %q}", ocf, "message")
}
want := "ssh: rejected: unknown reason 42 (message)"
if err.Error() != want {
t.Errorf("got %q, want %q", err.Error(), want)
}
}
func TestMuxChannelRequest(t *testing.T) {
client, server, mux := channelPair(t)
defer server.Close()
defer client.Close()
defer mux.Close()
var received int
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
for r := range server.incomingRequests {
received++
r.Reply(r.Type == "yes", nil)
}
wg.Done()
}()
_, err := client.SendRequest("yes", false, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
ok, err := client.SendRequest("yes", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if !ok {
t.Errorf("SendRequest(yes): %v", ok)
}
ok, err = client.SendRequest("no", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if ok {
t.Errorf("SendRequest(no): %v", ok)
}
client.Close()
wg.Wait()
if received != 3 {
t.Errorf("got %d requests, want %d", received, 3)
}
}
func TestMuxUnknownChannelRequests(t *testing.T) {
clientPipe, serverPipe := memPipe()
client := newMux(clientPipe)
defer serverPipe.Close()
defer client.Close()
kDone := make(chan error, 1)
go func() {
// Ignore unknown channel messages that don't want a reply.
err := serverPipe.writePacket(Marshal(channelRequestMsg{
PeersID: 1,
Request: "keepalive@openssh.com",
WantReply: false,
RequestSpecificData: []byte{},
}))
if err != nil {
kDone <- fmt.Errorf("send: %w", err)
return
}
// Send a keepalive, which should get a channel failure message
// in response.
err = serverPipe.writePacket(Marshal(channelRequestMsg{
PeersID: 2,
Request: "keepalive@openssh.com",
WantReply: true,
RequestSpecificData: []byte{},
}))
if err != nil {
kDone <- fmt.Errorf("send: %w", err)
return
}
packet, err := serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
decoded, err := decode(packet)
if err != nil {
kDone <- fmt.Errorf("decode failed: %w", err)
return
}
switch msg := decoded.(type) {
case *channelRequestFailureMsg:
if msg.PeersID != 2 {
kDone <- fmt.Errorf("received response to wrong message: %v", msg)
return
}
default:
kDone <- fmt.Errorf("unexpected channel message: %v", msg)
return
}
kDone <- nil
// Receive and respond to the keepalive to confirm the mux is
// still processing requests.
packet, err = serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
if packet[0] != msgGlobalRequest {
kDone <- errors.New("expected global request")
return
}
err = serverPipe.writePacket(Marshal(globalRequestFailureMsg{
Data: []byte{},
}))
if err != nil {
kDone <- fmt.Errorf("failed to send failure msg: %w", err)
return
}
close(kDone)
}()
// Wait for the server to send the keepalive message and receive back a
// response.
if err := <-kDone; err != nil {
t.Fatal(err)
}
// Confirm client hasn't closed.
if _, _, err := client.SendRequest("keepalive@golang.org", true, nil); err != nil {
t.Fatalf("failed to send keepalive: %v", err)
}
// Wait for the server to shut down.
if err := <-kDone; err != nil {
t.Fatal(err)
}
}
func TestMuxClosedChannel(t *testing.T) {
clientPipe, serverPipe := memPipe()
client := newMux(clientPipe)
defer serverPipe.Close()
defer client.Close()
kDone := make(chan error, 1)
go func() {
// Open the channel.
packet, err := serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
if packet[0] != msgChannelOpen {
kDone <- errors.New("expected chan open")
return
}
var openMsg channelOpenMsg
if err := Unmarshal(packet, &openMsg); err != nil {
kDone <- fmt.Errorf("unmarshal: %w", err)
return
}
// Send back the opened channel confirmation.
err = serverPipe.writePacket(Marshal(channelOpenConfirmMsg{
PeersID: openMsg.PeersID,
MyID: 0,
MyWindow: 0,
MaxPacketSize: channelMaxPacket,
}))
if err != nil {
kDone <- fmt.Errorf("send: %w", err)
return
}
// Close the channel.
err = serverPipe.writePacket(Marshal(channelCloseMsg{
PeersID: openMsg.PeersID,
}))
if err != nil {
kDone <- fmt.Errorf("send: %w", err)
return
}
// Send a keepalive message on the channel we just closed.
err = serverPipe.writePacket(Marshal(channelRequestMsg{
PeersID: openMsg.PeersID,
Request: "keepalive@openssh.com",
WantReply: true,
RequestSpecificData: []byte{},
}))
if err != nil {
kDone <- fmt.Errorf("send: %w", err)
return
}
// Receive the channel closed response.
packet, err = serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
if packet[0] != msgChannelClose {
kDone <- errors.New("expected channel close")
return
}
// Receive the keepalive response failure.
packet, err = serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
if packet[0] != msgChannelFailure {
kDone <- errors.New("expected channel failure")
return
}
kDone <- nil
// Receive and respond to the keepalive to confirm the mux is
// still processing requests.
packet, err = serverPipe.readPacket()
if err != nil {
kDone <- fmt.Errorf("read packet: %w", err)
return
}
if packet[0] != msgGlobalRequest {
kDone <- errors.New("expected global request")
return
}
err = serverPipe.writePacket(Marshal(globalRequestFailureMsg{
Data: []byte{},
}))
if err != nil {
kDone <- fmt.Errorf("failed to send failure msg: %w", err)
return
}
close(kDone)
}()
// Open a channel.
ch, err := client.openChannel("chan", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
defer ch.Close()
// Wait for the server to close the channel and send the keepalive.
<-kDone
// Make sure the channel closed.
if _, ok := <-ch.incomingRequests; ok {
t.Fatalf("channel not closed")
}
// Confirm client hasn't closed
if _, _, err := client.SendRequest("keepalive@golang.org", true, nil); err != nil {
t.Fatalf("failed to send keepalive: %v", err)
}
// Wait for the server to shut down.
<-kDone
}
func TestMuxGlobalRequest(t *testing.T) {
var sawPeek bool
var wg sync.WaitGroup
defer func() {
wg.Wait()
if !sawPeek {
t.Errorf("never saw 'peek' request")
}
}()
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
wg.Add(1)
go func() {
defer wg.Done()
for r := range serverMux.incomingRequests {
sawPeek = sawPeek || r.Type == "peek"
if r.WantReply {
err := r.Reply(r.Type == "yes",
append([]byte(r.Type), r.Payload...))
if err != nil {
t.Errorf("AckRequest: %v", err)
}
}
}
}()
_, _, err := clientMux.SendRequest("peek", false, nil)
if err != nil {
t.Errorf("SendRequest: %v", err)
}
ok, data, err := clientMux.SendRequest("yes", true, []byte("a"))
if !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("yes", true, []byte("a")); !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("no", true, []byte("a")); ok || string(data) != "noa" || err != nil {
t.Errorf("SendRequest(\"no\", true, \"a\"): %v %v %v",
ok, data, err)
}
}
func TestMuxGlobalRequestUnblock(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
result := make(chan error, 1)
go func() {
_, _, err := clientMux.SendRequest("hello", true, nil)
result <- err
}()
<-serverMux.incomingRequests
serverMux.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", io.EOF)
}
}
func TestMuxChannelRequestUnblock(t *testing.T) {
a, b, connB := channelPair(t)
defer a.Close()
defer b.Close()
defer connB.Close()
result := make(chan error, 1)
go func() {
_, err := a.SendRequest("hello", true, nil)
result <- err
}()
<-b.incomingRequests
connB.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", err)
}
}
func TestMuxCloseChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
defer r.Close()
defer w.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.Close(); err != nil {
t.Errorf("w.Close: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after Close", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxCloseWriteChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.CloseWrite(); err != nil {
t.Errorf("w.CloseWrite: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after CloseWrite", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxInvalidRecord(t *testing.T) {
a, b := muxPair()
defer a.Close()
defer b.Close()
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], 29348723 /* invalid channel id */)
marshalUint32(packet[5:], 1)
packet[9] = 42
a.conn.writePacket(packet)
go a.SendRequest("hello", false, nil)
// 'a' wrote an invalid packet, so 'b' has exited.
req, ok := <-b.incomingRequests
if ok {
t.Errorf("got request %#v after receiving invalid packet", req)
}
}
func TestZeroWindowAdjust(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
go func() {
io.WriteString(a, "hello")
// bogus adjust.
a.sendMessage(windowAdjustMsg{})
io.WriteString(a, "world")
a.Close()
}()
want := "helloworld"
c, _ := io.ReadAll(b)
if string(c) != want {
t.Errorf("got %q want %q", c, want)
}
}
func TestMuxMaxPacketSize(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
large := make([]byte, a.maxRemotePayload+1)
packet := make([]byte, 1+4+4+1+len(large))
packet[0] = msgChannelData
marshalUint32(packet[1:], a.remoteId)
marshalUint32(packet[5:], uint32(len(large)))
packet[9] = 42
if err := a.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
a.SendRequest("hello", false, nil)
wg.Done()
}()
_, ok := <-b.incomingRequests
if ok {
t.Errorf("connection still alive after receiving large packet.")
}
}
func TestMuxChannelWindowDeferredUpdates(t *testing.T) {
s, c, mux := channelPair(t)
cTransport := mux.conn.(*memTransport)
defer s.Close()
defer c.Close()
defer mux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
data := make([]byte, 1024)
wg.Add(1)
go func() {
defer wg.Done()
_, err := s.Write(data)
if err != nil {
t.Errorf("Write: %v", err)
return
}
}()
cWritesInit := cTransport.getWriteCount()
buf := make([]byte, 1)
for i := 0; i < len(data); i++ {
n, err := c.Read(buf)
if n != len(buf) || err != nil {
t.Fatalf("Read: %v, %v", n, err)
}
}
cWrites := cTransport.getWriteCount() - cWritesInit
// reading 1 KiB should not cause any window updates to be sent, but allow
// for some unexpected writes
if cWrites > 30 {
t.Fatalf("reading 1 KiB from channel caused %v writes", cWrites)
}
}
func TestMuxChannelRejectRemovesFromMux(t *testing.T) {
serverMux, clientMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
var wg sync.WaitGroup
t.Cleanup(wg.Wait)
wg.Add(1)
go func() {
defer wg.Done()
// The server waits for the channel creation request
newCh, ok := <-serverMux.incomingChannels
if !ok {
t.Error("failed to accept channel")
return
}
ch := newCh.(*channel)
if serverMux.chanList.getChan(ch.localId) == nil {
t.Errorf("channel %d is not in the chanList before Reject", ch.localId)
}
if err := ch.Reject(Prohibited, "rejecting this channel"); err != nil {
t.Errorf("Reject failed: %v", err)
}
if serverMux.chanList.getChan(ch.localId) != nil {
t.Errorf("channel %d is still in the chanList after Reject", ch.localId)
}
}()
_, _, err := clientMux.OpenChannel("test_leak", nil)
if err == nil {
t.Fatal("expected an error (channel rejected), but got nil")
}
if _, ok := err.(*OpenChannelError); !ok {
t.Errorf("expected *OpenChannelError, got: %T", err)
}
}
// Don't ship code with debug=true.
func TestDebug(t *testing.T) {
if debugMux {
t.Error("mux debug switched on")
}
if debugHandshake {
t.Error("handshake debug switched on")
}
if debugTransport {
t.Error("transport debug switched on")
}
}
func TestMuxUnexpectedGlobalResponsesDiscarded(t *testing.T) {
clientPipe, serverPipe := memPipe()
client := newMux(clientPipe)
defer serverPipe.Close()
defer client.Close()
done := make(chan error, 1)
go func() {
// Send multiple unexpected global responses, this should not block the
// globalResponses channel.
for i := range 5 {
err := serverPipe.writePacket(Marshal(globalRequestSuccessMsg{
Data: []byte{byte(i)},
}))
if err != nil {
done <- fmt.Errorf("send success msg %d: %w", i, err)
return
}
}
for i := range 5 {
err := serverPipe.writePacket(Marshal(globalRequestFailureMsg{
Data: []byte{byte(i)},
}))
if err != nil {
done <- fmt.Errorf("send failure msg %d: %w", i, err)
return
}
}
// Now send a global request and wait for the response. This
// verifies the mux is still processing packets.
err := serverPipe.writePacket(Marshal(globalRequestMsg{
Type: "keepalive@golang.org",
WantReply: true,
Data: nil,
}))
if err != nil {
done <- fmt.Errorf("send global request: %w", err)
return
}
packet, err := serverPipe.readPacket()
if err != nil {
done <- fmt.Errorf("read packet: %w", err)
return
}
decoded, err := decode(packet)
if err != nil {
done <- fmt.Errorf("decode: %w", err)
return
}
switch decoded.(type) {
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
// Expected response
default:
done <- fmt.Errorf("unexpected packet type: %T", decoded)
return
}
done <- nil
}()
// Handle the incoming request from the server and reply
req, ok := <-client.incomingRequests
if !ok {
t.Fatal("incomingRequests channel closed unexpectedly")
}
if req.Type != "keepalive@golang.org" {
t.Fatalf("unexpected request type: %s", req.Type)
}
if err := req.Reply(true, nil); err != nil {
t.Fatalf("Reply: %v", err)
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestMuxConcurrentGlobalRequests(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
const numRequests = 50
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
for r := range serverMux.incomingRequests {
if r.WantReply {
replyData := append([]byte("reply:"), r.Payload...)
r.Reply(true, replyData)
}
}
}()
var clientWg sync.WaitGroup
clientWg.Add(numRequests)
errCh := make(chan error, numRequests)
for i := range numRequests {
go func(id int) {
defer clientWg.Done()
payloadStr := fmt.Sprintf("req-%d", id)
payload := []byte(payloadStr)
// This call blocks until the globalSentMu is acquired.
// The mutex ensures that even with many concurrent attempts,
// the "drain" and "send" logic happens atomically per request.
ok, data, err := clientMux.SendRequest("echo", true, payload)
if err != nil {
errCh <- fmt.Errorf("req %d error: %v", id, err)
return
}
if !ok {
errCh <- fmt.Errorf("req %d failed (want success)", id)
return
}
expected := "reply:" + payloadStr
if string(data) != expected {
errCh <- fmt.Errorf("req %d mismatch: got %q, want %q", id, string(data), expected)
}
}(i)
}
clientWg.Wait()
close(errCh)
for err := range errCh {
if err != nil {
t.Fatal(err)
}
}
clientMux.Close()
<-serverDone
}
func TestMuxGlobalResponseDroppedWhenIdle(t *testing.T) {
clientPipe, serverPipe := memPipe()
clientMux := newMux(clientPipe)
defer serverPipe.Close()
defer clientMux.Close()
errCh := make(chan error, 1)
go func() {
// Send a spurious response while no SendRequest is pending.
if err := serverPipe.writePacket(Marshal(globalRequestSuccessMsg{
Data: []byte("spurious"),
})); err != nil {
errCh <- fmt.Errorf("send spurious: %w", err)
return
}
// Follow with a global request; once the client observes this on
// incomingRequests, the mux loop has necessarily processed (and
// dropped) the prior spurious response.
if err := serverPipe.writePacket(Marshal(globalRequestMsg{
Type: "sync@example.com",
WantReply: false,
})); err != nil {
errCh <- fmt.Errorf("send sync request: %w", err)
return
}
errCh <- nil
}()
if err := <-errCh; err != nil {
t.Fatal(err)
}
req, ok := <-clientMux.incomingRequests
if !ok {
t.Fatal("incomingRequests closed unexpectedly")
}
if req.Type != "sync@example.com" {
t.Fatalf("unexpected sync request type %q", req.Type)
}
// The spurious response preceded the sync request, so by now the mux
// loop has processed it. The pending-gate must have caused it to be
// dropped rather than buffered.
if n := len(clientMux.globalResponses); n != 0 {
t.Fatalf("globalResponses buffer should be empty after idle drop, has %d entries", n)
}
}
func TestMuxStaleResponseDrained(t *testing.T) {
// Simulate a stale response sitting in globalResponses (e.g. a response
// that slipped in through the pending-gate on a prior SendRequest that
// exited without consuming it). The drain step in the next SendRequest
// must discard it so the caller receives the correct reply.
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
clientMux.globalResponses <- &globalRequestSuccessMsg{Data: []byte("stale")}
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
for req := range serverMux.incomingRequests {
if req.WantReply {
req.Reply(true, append([]byte("reply:"), req.Payload...))
}
}
}()
ok, data, err := clientMux.SendRequest("test", true, []byte("hello"))
if err != nil {
t.Fatalf("request failed: %v", err)
}
if !ok {
t.Fatal("expected success response")
}
if string(data) != "reply:hello" {
t.Fatalf("got %q, want %q (drain did not remove stale response)", data, "reply:hello")
}
clientMux.Close()
<-serverDone
}
func TestMuxGlobalResponseAcceptedWhilePending(t *testing.T) {
// Positive control: when a SendRequest is actually pending, the
// response must be delivered (the gate is open).
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
for req := range serverMux.incomingRequests {
if req.WantReply {
req.Reply(true, []byte("pong"))
}
}
}()
ok, data, err := clientMux.SendRequest("ping", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if !ok || string(data) != "pong" {
t.Fatalf("unexpected response: ok=%v data=%q", ok, data)
}
clientMux.Close()
<-serverDone
}