package smux import ( "bytes" "io" "net" "sync" "testing" "time" ) func TestBufferRingPushPopOrder(t *testing.T) { r := newBufferRing(2) b1 := []byte{1} b2 := []byte{2} b3 := []byte{3} r.push(b1, &b1) r.push(b2, &b2) buf, head, ok := r.pop() if !ok || buf[0] != 1 || head == nil || (*head)[0] != 1 { t.Fatalf("unexpected pop result: ok=%v buf=%v head=%v", ok, buf, head) } r.push(b3, &b3) buf, head, ok = r.pop() if !ok || buf[0] != 2 || head == nil || (*head)[0] != 2 { t.Fatalf("unexpected pop result after wrap: ok=%v buf=%v head=%v", ok, buf, head) } buf, head, ok = r.pop() if !ok || buf[0] != 3 || head == nil || (*head)[0] != 3 { t.Fatalf("unexpected pop result after wrap 2: ok=%v buf=%v head=%v", ok, buf, head) } if r.size != 0 || r.head != r.tail { t.Fatalf("ring not empty after pops: size=%d head=%d tail=%d", r.size, r.head, r.tail) } } func TestBufferRingGrow(t *testing.T) { r := newBufferRing(2) b1 := []byte{1} b2 := []byte{2} b3 := []byte{3} r.push(b1, &b1) r.push(b2, &b2) r.push(b3, &b3) // trigger grow if len(r.bufs) < 3 { t.Fatalf("expected ring to grow, capacity=%d", len(r.bufs)) } buf, head, ok := r.pop() if !ok || buf[0] != 1 || head == nil || (*head)[0] != 1 { t.Fatalf("unexpected pop after grow: ok=%v buf=%v head=%v", ok, buf, head) } buf, head, ok = r.pop() if !ok || buf[0] != 2 || head == nil || (*head)[0] != 2 { t.Fatalf("unexpected pop after grow 2: ok=%v buf=%v head=%v", ok, buf, head) } buf, head, ok = r.pop() if !ok || buf[0] != 3 || head == nil || (*head)[0] != 3 { t.Fatalf("unexpected pop after grow 3: ok=%v buf=%v head=%v", ok, buf, head) } if r.size != 0 || r.head != r.tail { t.Fatalf("ring not empty after grow pops: size=%d head=%d tail=%d", r.size, r.head, r.tail) } } func TestBufferRingEmptyPop(t *testing.T) { r := newBufferRing(2) if buf, head, ok := r.pop(); ok || buf != nil || head != nil { t.Fatalf("expected empty pop, got ok=%v buf=%v head=%v", ok, buf, head) } } // setupHalfCloseServer creates a server that accepts streams and can be controlled for half-close tests func setupHalfCloseServer(t *testing.T) (addr string, stopfunc func(), client net.Conn, serverSession chan *Session, err error) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { return "", nil, nil, nil, err } serverSession = make(chan *Session, 1) go func() { conn, err := ln.Accept() if err != nil { return } sess, _ := Server(conn, nil) serverSession <- sess }() addr = ln.Addr().String() conn, err := net.Dial("tcp", addr) if err != nil { ln.Close() return "", nil, nil, nil, err } return ln.Addr().String(), func() { ln.Close() }, conn, serverSession, nil } // TestHalfCloseBasic tests that CloseWrite sends FIN but allows reading func TestHalfCloseBasic(t *testing.T) { _, stop, cli, serverSessionCh, err := setupHalfCloseServer(t) if err != nil { t.Fatal(err) } defer stop() // Create client session clientSession, _ := Client(cli, nil) defer clientSession.Close() // Get server session serverSession := <-serverSessionCh defer serverSession.Close() // Open stream from client clientStream, err := clientSession.OpenStream() if err != nil { t.Fatal(err) } // Accept stream on server serverStream, err := serverSession.AcceptStream() if err != nil { t.Fatal(err) } // Client writes data then half-closes testData := []byte("hello from client") _, err = clientStream.Write(testData) if err != nil { t.Fatal("client write failed:", err) } // Client closes write side (half-close) err = clientStream.CloseWrite() if err != nil { t.Fatal("CloseWrite failed:", err) } // Server should be able to read the data buf := make([]byte, len(testData)) n, err := io.ReadFull(serverStream, buf) if err != nil { t.Fatal("server read failed:", err) } if !bytes.Equal(buf[:n], testData) { t.Fatalf("data mismatch: got %v, want %v", buf[:n], testData) } // Server should get EOF on next read (because client sent FIN) _, err = serverStream.Read(buf) if err != io.EOF { t.Fatalf("expected EOF after CloseWrite, got %v", err) } // Server should still be able to write back responseData := []byte("response from server") _, err = serverStream.Write(responseData) if err != nil { t.Fatal("server write failed after client CloseWrite:", err) } // Client should be able to read the response responseBuf := make([]byte, len(responseData)) n, err = io.ReadFull(clientStream, responseBuf) if err != nil { t.Fatal("client read failed:", err) } if !bytes.Equal(responseBuf[:n], responseData) { t.Fatalf("response mismatch: got %v, want %v", responseBuf[:n], responseData) } // Client write should fail after CloseWrite _, err = clientStream.Write([]byte("should fail")) if err != io.ErrClosedPipe { t.Fatalf("write after CloseWrite should return io.ErrClosedPipe, got %v", err) } } // TestHalfCloseDoubleCloseWrite tests that calling CloseWrite twice returns error func TestHalfCloseDoubleCloseWrite(t *testing.T) { _, stop, cli, serverSessionCh, err := setupHalfCloseServer(t) if err != nil { t.Fatal(err) } defer stop() clientSession, _ := Client(cli, nil) defer clientSession.Close() serverSession := <-serverSessionCh defer serverSession.Close() clientStream, _ := clientSession.OpenStream() serverSession.AcceptStream() // accept to avoid blocking // First CloseWrite should succeed err = clientStream.CloseWrite() if err != nil { t.Fatal("first CloseWrite failed:", err) } // Second CloseWrite should return error err = clientStream.CloseWrite() if err != io.ErrClosedPipe { t.Fatalf("second CloseWrite should return io.ErrClosedPipe, got %v", err) } } // TestHalfCloseBidirectional tests both sides doing half-close func TestHalfCloseBidirectional(t *testing.T) { _, stop, cli, serverSessionCh, err := setupHalfCloseServer(t) if err != nil { t.Fatal(err) } defer stop() clientSession, _ := Client(cli, nil) defer clientSession.Close() serverSession := <-serverSessionCh defer serverSession.Close() clientStream, _ := clientSession.OpenStream() serverStream, _ := serverSession.AcceptStream() // Use channels to coordinate the test clientWriteDone := make(chan struct{}) serverWriteDone := make(chan struct{}) var wg sync.WaitGroup wg.Add(2) // Client writes, waits for server write, then half-closes and reads go func() { defer wg.Done() clientStream.Write([]byte("client data")) close(clientWriteDone) // Wait for server to write before closing <-serverWriteDone // Read first, then close write buf := make([]byte, 100) n, err := clientStream.Read(buf) if err != nil && err != io.EOF { t.Errorf("client read failed: %v", err) return } if string(buf[:n]) != "server data" { t.Errorf("client got wrong data: %s", buf[:n]) } clientStream.CloseWrite() }() // Server writes, waits for client write, then half-closes and reads go func() { defer wg.Done() serverStream.Write([]byte("server data")) close(serverWriteDone) // Wait for client to write before closing <-clientWriteDone // Read first, then close write buf := make([]byte, 100) n, err := serverStream.Read(buf) if err != nil && err != io.EOF { t.Errorf("server read failed: %v", err) return } if string(buf[:n]) != "client data" { t.Errorf("server got wrong data: %s", buf[:n]) } serverStream.CloseWrite() }() wg.Wait() } // TestHalfCloseWithFullClose tests that Close() still works correctly func TestHalfCloseWithFullClose(t *testing.T) { _, stop, cli, serverSessionCh, err := setupHalfCloseServer(t) if err != nil { t.Fatal(err) } defer stop() clientSession, _ := Client(cli, nil) defer clientSession.Close() serverSession := <-serverSessionCh defer serverSession.Close() clientStream, _ := clientSession.OpenStream() serverStream, _ := serverSession.AcceptStream() // Write some data clientStream.Write([]byte("hello")) // Full close should still work err = clientStream.Close() if err != nil { t.Fatal("Close failed:", err) } // Double close should return error err = clientStream.Close() if err != io.ErrClosedPipe { t.Fatalf("double Close should return io.ErrClosedPipe, got %v", err) } // Server should get EOF buf := make([]byte, 100) serverStream.SetReadDeadline(time.Now().Add(time.Second)) for { _, err = serverStream.Read(buf) if err == io.EOF { break } if err != nil { t.Fatal("unexpected error:", err) } } } func TestHalfCloseAutoCleanup(t *testing.T) { _, stop, cli, serverSessionCh, err := setupHalfCloseServer(t) if err != nil { t.Fatal(err) } defer stop() clientSession, _ := Client(cli, nil) defer clientSession.Close() serverSession := <-serverSessionCh defer serverSession.Close() clientStream, _ := clientSession.OpenStream() serverStream, _ := serverSession.AcceptStream() if err := clientStream.CloseWrite(); err != nil { t.Fatal("client CloseWrite failed:", err) } if err := serverStream.CloseWrite(); err != nil { t.Fatal("server CloseWrite failed:", err) } deadline := time.Now().Add(time.Second) for { if clientSession.NumStreams() == 0 && serverSession.NumStreams() == 0 { break } if time.Now().After(deadline) { t.Fatalf("streams not cleaned up: client=%d server=%d", clientSession.NumStreams(), serverSession.NumStreams()) } time.Sleep(10 * time.Millisecond) } } // TestHalfCloseV2 tests half-close with protocol version 2 func TestHalfCloseV2(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatal(err) } defer ln.Close() config := DefaultConfig() config.Version = 2 serverSessionCh := make(chan *Session, 1) go func() { conn, err := ln.Accept() if err != nil { return } sess, _ := Server(conn, config) serverSessionCh <- sess }() conn, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Fatal(err) } clientSession, _ := Client(conn, config) defer clientSession.Close() serverSession := <-serverSessionCh defer serverSession.Close() clientStream, _ := clientSession.OpenStream() serverStream, _ := serverSession.AcceptStream() // Client writes data then half-closes testData := []byte("hello v2") clientStream.Write(testData) clientStream.CloseWrite() // Server reads data buf := make([]byte, len(testData)) io.ReadFull(serverStream, buf) if !bytes.Equal(buf, testData) { t.Fatalf("data mismatch: got %v, want %v", buf, testData) } // Server should get EOF _, err = serverStream.Read(buf) if err != io.EOF { t.Fatalf("expected EOF, got %v", err) } // Server can still write responseData := []byte("response v2") _, err = serverStream.Write(responseData) if err != nil { t.Fatal("server write failed:", err) } // Client can still read responseBuf := make([]byte, len(responseData)) _, err = io.ReadFull(clientStream, responseBuf) if err != nil { t.Fatal("client read failed:", err) } if !bytes.Equal(responseBuf, responseData) { t.Fatalf("response mismatch: got %v, want %v", responseBuf, responseData) } }