320 lines
8.6 KiB
Go
320 lines
8.6 KiB
Go
package teamspeak
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/honeybbq/teamspeak-go/commands"
|
|
"github.com/honeybbq/teamspeak-go/crypto"
|
|
"github.com/honeybbq/teamspeak-go/transport"
|
|
)
|
|
|
|
func TestHandleInitServer_SetsStatusConnected(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
c.handleCommand("initserver aclid=7 virtualserver_name=TestServer")
|
|
|
|
c.mu.Lock()
|
|
status := c.status
|
|
clid := c.clid
|
|
c.mu.Unlock()
|
|
|
|
if status != StatusConnected {
|
|
t.Errorf("expected StatusConnected, got %v", status)
|
|
}
|
|
if clid != 7 {
|
|
t.Errorf("expected clid=7, got %d", clid)
|
|
}
|
|
}
|
|
|
|
func TestHandleInitServer_FallsBackToClid(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
c.handleCommand("initserver clid=42 virtualserver_name=X")
|
|
|
|
c.mu.Lock()
|
|
clid := c.clid
|
|
c.mu.Unlock()
|
|
|
|
if clid != 42 {
|
|
t.Errorf("expected clid=42, got %d", clid)
|
|
}
|
|
}
|
|
|
|
func TestHandleInitServer_ClosesConnectedChan(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
c.handleCommand("initserver aclid=3 virtualserver_name=X")
|
|
|
|
select {
|
|
case <-c.connectedChan:
|
|
case <-time.After(time.Second):
|
|
t.Error("connectedChan was not closed after initserver")
|
|
}
|
|
}
|
|
|
|
func TestHandleInitServer_TriggersOnConnected(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
connected := make(chan struct{}, 1)
|
|
c.OnConnected(func() { connected <- struct{}{} })
|
|
|
|
c.handleCommand("initserver aclid=1")
|
|
|
|
select {
|
|
case <-connected:
|
|
case <-time.After(time.Second):
|
|
t.Error("OnConnected not called after initserver")
|
|
}
|
|
}
|
|
|
|
func TestHandleInitServer_CalledTwice_StillConnected(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
c.handleCommand("initserver aclid=1")
|
|
c.handleCommand("initserver aclid=2")
|
|
|
|
c.mu.Lock()
|
|
clid := c.clid
|
|
c.mu.Unlock()
|
|
|
|
if clid != 2 {
|
|
t.Errorf("expected clid=2 after second initserver, got %d", clid)
|
|
}
|
|
}
|
|
|
|
// handleHandshakeInitIV (old path)
|
|
|
|
func TestHandleHandshakeInitIV_InvalidAlpha_NoSendClientInit(t *testing.T) {
|
|
c := newTestClient(t)
|
|
// Invalid base64 for alpha: InitCrypto fails, handleHandshakeInitIV returns.
|
|
// Must not panic.
|
|
c.handleCommand("clientinitiv alpha=!!! beta=AAAAAAAAAA omega=")
|
|
}
|
|
|
|
func TestHandleHandshakeInitIV_ValidParams_SendsClientInit(t *testing.T) {
|
|
c, serverConn := newTestClientWithPipe(t)
|
|
|
|
// Drain the initial Init1 packet from Start().
|
|
_ = readFromPipe(t, serverConn)
|
|
|
|
// Wire OnPacket so that the handler routes Init1 packets to handlePacket.
|
|
c.handler.OnPacket = c.handlePacket
|
|
c.handler.OnClosed = func(err error) {}
|
|
|
|
// A valid old-style handshake: alpha + beta are 10-byte base64, omega is the
|
|
// server's public key. We use the test identity's own public key as omega
|
|
// (InitCrypto only validates format, not that it comes from a server).
|
|
pubKey := c.crypt.Identity.PublicKeyBase64()
|
|
|
|
cmd := "clientinitiv alpha=AAAAAAAAAA== beta=AAAAAAAAAA== omega=" + pubKey
|
|
c.handleCommand(cmd)
|
|
|
|
// After successful InitCrypto, sendClientInit is called and a Command packet
|
|
// (type 2) is sent through the handler. The payload is encrypted.
|
|
pkt := readFromPipe(t, serverConn)
|
|
if len(pkt) < 13 {
|
|
t.Fatalf("expected clientinit packet, got %d bytes", len(pkt))
|
|
}
|
|
// C2S header[4] byte (index 12 in S2C layout) = TypeFlagged; lower nibble = type.
|
|
pktType := pkt[12] & 0x0F
|
|
if pktType != 0x02 {
|
|
t.Errorf("expected PacketTypeCommand (2), got %d", pktType)
|
|
}
|
|
}
|
|
|
|
// handleHandshakeExpand2 (new path)
|
|
|
|
func TestHandleHandshakeExpand2_InvalidBeta_NoSendClientInit(t *testing.T) {
|
|
c := newTestClient(t)
|
|
// Invalid base64 for beta: DecodeString fails, returns early.
|
|
c.handleCommand("initivexpand2 l= omega= proof= beta=!!!")
|
|
}
|
|
|
|
func TestBuildClientInitCommand_DefaultAuthFieldsAreEmpty(t *testing.T) {
|
|
c := newTestClient(t)
|
|
|
|
cmd := commands.ParseCommand(c.buildClientInitCommand())
|
|
if cmd == nil {
|
|
t.Fatal("expected clientinit command")
|
|
}
|
|
|
|
if got := cmd.Params["client_default_channel"]; got != "" {
|
|
t.Errorf("expected empty client_default_channel, got %q", got)
|
|
}
|
|
if got := cmd.Params["client_default_channel_password"]; got != "" {
|
|
t.Errorf("expected empty client_default_channel_password, got %q", got)
|
|
}
|
|
if got := cmd.Params["client_server_password"]; got != "" {
|
|
t.Errorf("expected empty client_server_password, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildClientInitCommand_IncludesConfiguredHandshakeCredentials(t *testing.T) {
|
|
id, err := crypto.IdentityFromString(testClientIdentity)
|
|
if err != nil {
|
|
t.Fatalf("IdentityFromString: %v", err)
|
|
}
|
|
|
|
c := NewClient(
|
|
id,
|
|
"127.0.0.1:9987",
|
|
"Test Bot",
|
|
WithServerPassword("server secret"),
|
|
WithDefaultChannel("Lobby Alpha"),
|
|
WithDefaultChannelPassword("channel secret"),
|
|
)
|
|
|
|
cmd := commands.ParseCommand(c.buildClientInitCommand())
|
|
if cmd == nil {
|
|
t.Fatal("expected clientinit command")
|
|
}
|
|
|
|
if got := cmd.Params["client_server_password"]; got != prepareClientPassword("server secret") {
|
|
t.Errorf("expected client_server_password to be %q, got %q", prepareClientPassword("server secret"), got)
|
|
}
|
|
if got := cmd.Params["client_default_channel"]; got != "Lobby Alpha" {
|
|
t.Errorf("expected client_default_channel to be %q, got %q", "Lobby Alpha", got)
|
|
}
|
|
if got := cmd.Params["client_default_channel_password"]; got != prepareClientPassword("channel secret") {
|
|
t.Errorf(
|
|
"expected client_default_channel_password to be %q, got %q",
|
|
prepareClientPassword("channel secret"),
|
|
got,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestBuildClientInitCommand_PreservesCredentialFieldOrder(t *testing.T) {
|
|
id, err := crypto.IdentityFromString(testClientIdentity)
|
|
if err != nil {
|
|
t.Fatalf("IdentityFromString: %v", err)
|
|
}
|
|
|
|
c := NewClient(
|
|
id,
|
|
"127.0.0.1:9987",
|
|
"Test Bot",
|
|
WithServerPassword("server secret"),
|
|
WithDefaultChannel("Lobby Alpha"),
|
|
WithDefaultChannelPassword("channel secret"),
|
|
)
|
|
|
|
raw := c.buildClientInitCommand()
|
|
defaultChannelIndex := indexOfOrFail(t, raw, "client_default_channel=Lobby\\sAlpha")
|
|
defaultChannelPasswordIndex := indexOfOrFail(
|
|
t,
|
|
raw,
|
|
"client_default_channel_password="+commands.Escape(prepareClientPassword("channel secret")),
|
|
)
|
|
serverPasswordIndex := indexOfOrFail(
|
|
t,
|
|
raw,
|
|
"client_server_password="+commands.Escape(prepareClientPassword("server secret")),
|
|
)
|
|
metaDataIndex := indexOfOrFail(t, raw, "client_meta_data=")
|
|
|
|
if defaultChannelIndex >= defaultChannelPasswordIndex ||
|
|
defaultChannelPasswordIndex >= serverPasswordIndex ||
|
|
serverPasswordIndex >= metaDataIndex {
|
|
t.Fatalf("unexpected credential field order in %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestHandlePacket_Init1_Step0_SendsStep1(t *testing.T) {
|
|
c, serverConn := newTestClientWithPipe(t)
|
|
c.handler.OnPacket = c.handlePacket
|
|
c.handler.OnClosed = func(error) {}
|
|
|
|
// Drain initial C2S Init1 from Start().
|
|
_ = readFromPipe(t, serverConn)
|
|
|
|
// Build step-0 server response: data[0]=0x00
|
|
step0 := make([]byte, 21)
|
|
step0[0] = 0x00
|
|
binary.LittleEndian.PutUint32(step0[9:13], 0xCAFEBABE)
|
|
|
|
// Wrap as S2C Init1 raw packet: [8 tag][2 ID][1 TypeFlagged][21 payload]
|
|
pktBytes := make([]byte, 8+3+len(step0))
|
|
pktBytes[10] = 0x08 // PacketTypeInit1
|
|
copy(pktBytes[11:], step0)
|
|
_, writeErr := serverConn.Write(pktBytes)
|
|
if writeErr != nil {
|
|
t.Fatalf("Write: %v", writeErr)
|
|
}
|
|
|
|
// handlePacket calls handler.SendPacket(8, step1Response, 0),
|
|
// which writes through the pipe.
|
|
resp := readFromPipe(t, serverConn)
|
|
if len(resp) < 13 {
|
|
t.Fatalf("expected step-1 response, got %d bytes", len(resp))
|
|
}
|
|
if resp[12]&0x0F != 0x08 {
|
|
t.Errorf("expected PacketTypeInit1 (8), got %d", resp[12]&0x0F)
|
|
}
|
|
// Step-1 payload starts with 0x01
|
|
if resp[13] != 0x01 {
|
|
t.Errorf("expected step 0x01 payload, got 0x%02x", resp[13])
|
|
}
|
|
}
|
|
|
|
func TestHandlePacket_CommandType_RoutedToHandleCommandLines(t *testing.T) {
|
|
c := newTestClient(t)
|
|
entered := make(chan struct{}, 1)
|
|
c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
|
|
c.rebuildMiddlewareChains()
|
|
|
|
enterLine := "notifycliententerview clid=9 client_nickname=G" +
|
|
" cid=1 client_type=0 client_servergroups= client_unique_identifier=g"
|
|
p := &transport.Packet{
|
|
TypeFlagged: 0x02, // PacketTypeCommand
|
|
Data: []byte(enterLine),
|
|
}
|
|
c.handlePacket(p)
|
|
|
|
select {
|
|
case <-entered:
|
|
case <-time.After(time.Second):
|
|
t.Error("handlePacket did not route Command to handleCommandLines")
|
|
}
|
|
}
|
|
|
|
// readFromPipe reads one datagram from the server-side pipe with a 2s timeout.
|
|
func readFromPipe(t *testing.T, server *pipePair) []byte {
|
|
t.Helper()
|
|
buf := make([]byte, 4096)
|
|
done := make(chan []byte, 1)
|
|
go func() {
|
|
n, readErr := server.Read(buf)
|
|
if readErr != nil {
|
|
done <- nil
|
|
|
|
return
|
|
}
|
|
cp := make([]byte, n)
|
|
copy(cp, buf[:n])
|
|
done <- cp
|
|
}()
|
|
select {
|
|
case data := <-done:
|
|
return data
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("readFromPipe: timed out")
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func indexOfOrFail(t *testing.T, s string, needle string) int {
|
|
t.Helper()
|
|
|
|
idx := strings.Index(s, needle)
|
|
if idx < 0 {
|
|
t.Fatalf("expected %q to contain %q", s, needle)
|
|
}
|
|
|
|
return idx
|
|
}
|