首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
@@ -0,0 +1,319 @@
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
}