Files
go-ts/internal/client/client.go
Jose Luis Montañes Ojados 8d98579faa client can join to channel
2026-01-15 17:09:32 +01:00

672 lines
21 KiB
Go

package client
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"strings"
"time"
"go-ts/pkg/protocol"
"go-ts/pkg/transport"
"github.com/dgryski/go-quicklz"
)
type Channel struct {
ID uint64
ParentID uint64
Name string
Order uint64
}
type Client struct {
Conn *transport.TS3Conn
Handshake *HandshakeState
Nickname string
ClientID uint16
// Counters
PacketIDCounterC2S uint16
// State
Connected bool
// Fragment reassembly
FragmentBuffer []byte
FragmentStartPktID uint16
FragmentCompressed bool
Fragmenting bool
// Server Data
Channels map[uint64]*Channel
}
func NewClient(nickname string) *Client {
return &Client{
Nickname: nickname,
PacketIDCounterC2S: 1,
Channels: make(map[uint64]*Channel),
}
}
func (c *Client) Connect(address string) error {
conn, err := transport.NewTS3Conn(address)
if err != nil {
return err
}
c.Conn = conn
// Initialize handshake state
hs, err := NewHandshakeState(c.Conn)
if err != nil {
return err
}
c.Handshake = hs
// Improve Identity Security Level to 8 (Standard Requirement)
c.Handshake.ImproveSecurityLevel(8)
log.Println("Connected to UDP. Starting Handshake...")
// Start Handshake Flow
// Step 0
if err := c.Handshake.SendPacket0(); err != nil {
return err
}
// Read Loop for Handshake
timeout := time.After(5 * time.Second)
for !c.Connected {
select {
case pkt := <-c.Conn.PacketChan():
if err := c.handlePacket(pkt); err != nil {
log.Printf("Error handling packet: %v", err)
}
case <-timeout:
return fmt.Errorf("connection timed out")
}
}
log.Println("=== Connected! Now listening for server data... ===")
// Send Ping every 3 seconds
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
// KeepAlive Loop
for {
select {
case pkt := <-c.Conn.PacketChan():
if err := c.handlePacket(pkt); err != nil {
log.Printf("Error handling packet: %v", err)
}
case <-ticker.C:
// Send Ping
c.PacketIDCounterC2S++
ping := protocol.NewPacket(protocol.PacketTypePing, nil)
ping.Header.PacketID = c.PacketIDCounterC2S
ping.Header.ClientID = c.ClientID // Should be assigned by server usually, but we use 0 or what?
// Encrypt Ping (if past handshake)
// For now, assuming unencrypted ping is ignored or we need to encrypt it if in full session
// Protocol says: "Everything is encrypted"
// Using correct keys...
// Actually handlePacket sends PONG. We need to Initiate PING?
// Simplified: Just printing "Ping" for now, or just wait for server to Ping us.
// The server usually pings. We must reply Pong.
// BUT if we don't send anything, we might time out.
// Let's rely on Server Pings for now, but remove the 5s exit timeout.
}
}
}
func (c *Client) handlePacket(pkt *protocol.Packet) error {
log.Printf("Received Packet: ID=%d, Type=%v, Len=%d", pkt.Header.PacketID, pkt.Header.PacketType(), len(pkt.Data))
switch pkt.Header.PacketType() {
case protocol.PacketTypeInit1:
return c.handleInit(pkt)
case protocol.PacketTypeCommand:
// Send ACK
// Ack Data: PacketID of the packet we're acknowledging (2 bytes)
ackData := make([]byte, 2)
binary.BigEndian.PutUint16(ackData, pkt.Header.PacketID)
ack := protocol.NewPacket(protocol.PacketTypeAck, ackData)
// ACK header PacketID should match the packet being acknowledged
ack.Header.PacketID = pkt.Header.PacketID
// ACKs for Command packets during handshake are encrypted with HandshakeKey
key := protocol.HandshakeKey
nonce := protocol.HandshakeNonce
// Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], ack.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], ack.Header.ClientID) // ClientID (usually 0 during handshake)
meta[4] = ack.Header.Type
encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, ack.Data)
ack.Data = encData
copy(ack.Header.MAC[:], mac)
log.Printf("Sending ACK for PacketID %d", pkt.Header.PacketID)
c.Conn.SendPacket(ack)
return c.handleCommand(pkt)
case protocol.PacketTypeVoice:
c.handleVoice(pkt)
case protocol.PacketTypePing:
// Respond with Pong
pong := protocol.NewPacket(protocol.PacketTypePong, nil)
pong.Header.PacketID = pkt.Header.PacketID // Acknowledgement
pong.Header.MAC = pkt.Header.MAC // TODO: calculate real mac
c.Conn.SendPacket(pong)
case protocol.PacketTypeAck:
// Server acknowledged our packet - ACKs are encrypted
// Decrypt with HandshakeKey
key := protocol.HandshakeKey
nonce := protocol.HandshakeNonce
meta := make([]byte, 3) // Server->Client is 3 bytes
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
meta[2] = pkt.Header.Type
data, err := protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
if err != nil {
log.Printf("ACK decryption failed: %v", err)
return nil
}
ackPId := uint16(0)
if len(data) >= 2 {
ackPId = binary.BigEndian.Uint16(data[0:2])
}
log.Printf("Received ACK for PacketID %d", ackPId)
// If ACK is for clientek (PID=1), proceed with clientinit
if ackPId == 1 && c.Handshake != nil && c.Handshake.Step == 5 {
log.Println("clientek acknowledged! Sending clientinit...")
c.Handshake.Step = 6
return c.sendClientInit()
}
// If ACK is for clientinit (PID=2), we're connected!
if ackPId == 2 && c.Handshake != nil && c.Handshake.Step == 6 {
log.Println("clientinit acknowledged! Connection established!")
c.Connected = true
}
}
return nil
}
func (c *Client) handleInit(pkt *protocol.Packet) error {
// Determine step based on packet content or local state
// Simple state machine
if c.Handshake.Step == 0 {
if err := c.Handshake.HandlePacket1(pkt); err != nil {
return err
}
log.Println("Handshake Step 1 Completed. Sending Step 2...")
return c.Handshake.SendPacket2()
} else if c.Handshake.Step == 1 {
// Wait, step 1 is processed, we sent step 2.
// We expect Step 3.
if pkt.Data[0] == 0x03 {
if err := c.Handshake.HandlePacket3(pkt); err != nil {
return err
}
log.Println("Handshake Step 3 Completed. Sending Step 4 (Puzzle Solution)...")
// Send Packet 4 (Not fully implemented in this snippet due to puzzle complexity)
// c.Handshake.SendPacket4()
}
}
return nil
}
func (c *Client) handleCommand(pkt *protocol.Packet) error {
// Check if Encrypted
// PacketTypeCommand is usually encrypted.
// Flag check? The flag is in the Header (e.g. Unencrypted flag).
// If Unencrypted flag is SET, it's cleartext.
// Spec: "Command ... Encrypted: ✓". So Unencrypted flag is CLEARED.
// Decrypt if necessary
var data []byte
var err error
if pkt.Header.FlagUnencrypted() {
data = pkt.Data
} else {
var key, nonce []byte
decrypted := false
// 1. Try SharedSecret if available
if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 {
// Use SharedSecret-based encryption
crypto := &protocol.CryptoState{
SharedIV: c.Handshake.SharedIV,
SharedMac: c.Handshake.SharedMac,
GenerationID: 0,
}
// Server->Client = false
key, nonce = crypto.GenerateKeyNonce(&pkt.Header, false)
// AAD for Server->Client: PacketID (2) + Type|Flags (1)
meta := make([]byte, 3)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
meta[2] = pkt.Header.Type // Type includes Flags
data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
if err == nil {
decrypted = true
} else {
log.Printf("SharedSecret decrypt failed (PID=%d): %v. Trying HandshakeKey...", pkt.Header.PacketID, err)
}
}
// 2. Fallback to HandshakeKey
if !decrypted {
key = protocol.HandshakeKey[:]
nonce = protocol.HandshakeNonce[:]
// AAD matching KeyNonce derivation context?
// HandshakeKey usage usually has same AAD requirements?
meta := make([]byte, 3)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
meta[2] = pkt.Header.Type // Type includes Flags
data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
if err != nil {
log.Printf("All decryption attempts failed for PID=%d: %v", pkt.Header.PacketID, err)
return fmt.Errorf("decryption failed: %v", err)
}
}
}
// On first encrypted command set Connected = true (Fallback if ACK missed)
if !c.Connected && pkt.Header.PacketID > 2 {
c.Connected = true
}
// Fragment reassembly logic:
// - First fragment: Fragmented=true, optionally Compressed=true -> start buffer
// - Middle fragments: Fragmented=false, Compressed=false -> append to buffer
// - Last fragment: Fragmented=true -> append and process
isFragmented := pkt.Header.FlagFragmented()
if isFragmented && !c.Fragmenting {
// First fragment - start collecting
c.Fragmenting = true
c.FragmentBuffer = make([]byte, 0, 4096)
c.FragmentBuffer = append(c.FragmentBuffer, data...)
c.FragmentStartPktID = pkt.Header.PacketID
c.FragmentCompressed = pkt.Header.FlagCompressed()
log.Printf("Fragment start (PID=%d, Compressed=%v, Len=%d)", pkt.Header.PacketID, c.FragmentCompressed, len(data))
return nil // Wait for more fragments
} else if c.Fragmenting && !isFragmented {
// Middle fragment - append
c.FragmentBuffer = append(c.FragmentBuffer, data...)
log.Printf("Fragment continue (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer))
return nil // Wait for more fragments
} else if c.Fragmenting && isFragmented {
// Last fragment - complete reassembly
c.FragmentBuffer = append(c.FragmentBuffer, data...)
log.Printf("Fragment end (PID=%d, TotalLen=%d)", pkt.Header.PacketID, len(c.FragmentBuffer))
data = c.FragmentBuffer
// Decompress if first fragment was compressed
if c.FragmentCompressed {
decompressed, err := quicklz.Decompress(data)
if err != nil {
log.Printf("QuickLZ decompression of fragmented data failed: %v", err)
// Fallback to raw data
} else {
log.Printf("Decompressed fragmented: %d -> %d bytes", len(data), len(decompressed))
data = decompressed
}
}
// Reset fragment state
c.Fragmenting = false
c.FragmentBuffer = nil
} else {
// Non-fragmented packet - decompress if needed
if pkt.Header.FlagCompressed() {
decompressed, err := quicklz.Decompress(data)
if err != nil {
log.Printf("QuickLZ decompression failed: %v (falling back to raw)", err)
// Fallback to raw data - might not be compressed despite flag
} else {
log.Printf("Decompressed: %d -> %d bytes", len(data), len(decompressed))
data = decompressed
}
}
}
cmdStr := string(data)
// Debug: Log packet flags and raw command preview
log.Printf("Debug Packet: Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q",
pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data),
func() string {
if len(cmdStr) > 100 {
return cmdStr[:100]
}
return cmdStr
}())
// Fix Garbage Headers (TS3 often sends binary garbage before command)
// Scan for first valid lower case [a-z] char (Most commands are lowercase)
validStart := strings.IndexFunc(cmdStr, func(r rune) bool {
return (r >= 'a' && r <= 'z')
})
if validStart > 0 && validStart < 50 {
cmdStr = cmdStr[validStart:]
}
log.Printf("Command: %s", cmdStr)
// Parse Command
cmd, args := protocol.ParseCommand([]byte(cmdStr))
switch cmd {
case "initivexpand2":
err := c.Handshake.ProcessInitivexpand2(args)
if err != nil {
log.Printf("Error processing initivexpand2: %v", err)
}
case "initserver":
// Server sends this after clientinit - contains our clientID
if cid, ok := args["aclid"]; ok {
var id uint64
fmt.Sscanf(cid, "%d", &id)
c.ClientID = uint16(id)
log.Printf("Assigned ClientID: %d", c.ClientID)
}
if name, ok := args["virtualserver_name"]; ok {
log.Printf("Server Name: %s", protocol.Unescape(name))
}
case "channellist":
// Parse channel info
ch := &Channel{}
if cid, ok := args["cid"]; ok {
fmt.Sscanf(cid, "%d", &ch.ID)
}
if pid, ok := args["cpid"]; ok {
fmt.Sscanf(pid, "%d", &ch.ParentID)
}
if name, ok := args["channel_name"]; ok {
ch.Name = protocol.Unescape(name)
}
if order, ok := args["channel_order"]; ok {
fmt.Sscanf(order, "%d", &ch.Order)
}
c.Channels[ch.ID] = ch
log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args)
case "channellistfinished":
log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels))
var targetChan *Channel
for _, ch := range c.Channels {
log.Printf(" - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID)
if ch.Name == "Test" {
targetChan = ch
}
}
if targetChan == nil {
if ch, ok := c.Channels[2]; ok {
log.Printf("Name parsing failed. Defaulting to Channel 2 as 'Test'.")
targetChan = ch
}
}
if targetChan != nil {
log.Printf("Found target channel 'Test' (ID=%d). Joining...", targetChan.ID)
if c.ClientID == 0 {
log.Println("ERROR: ClientID is 0. Cannot join channel. 'initserver' missing?")
return nil
}
// clientmove clid={clid} cid={cid} cpw=
cmd := fmt.Sprintf("clientmove clid=%d cid=%d cpw=", c.ClientID, targetChan.ID)
pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd))
// Set NewProtocol flag (required for all commands) BEFORE computing meta
pkt.Header.Type |= protocol.PacketFlagNewProtocol
pkt.Header.PacketID = c.PacketIDCounterC2S + 1
pkt.Header.ClientID = c.ClientID
c.PacketIDCounterC2S++
// Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
meta[4] = pkt.Header.Type // Now includes NewProtocol flag
crypto := &protocol.CryptoState{
SharedIV: c.Handshake.SharedIV,
SharedMac: c.Handshake.SharedMac,
GenerationID: 0,
}
k, n := crypto.GenerateKeyNonce(&pkt.Header, true)
encData, mac, _ := protocol.EncryptEAX(k, n, meta, pkt.Data)
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
log.Printf("Sending clientmove command: clid=%d cid=%d (PID=%d)", c.ClientID, targetChan.ID, pkt.Header.PacketID)
c.Conn.SendPacket(pkt)
}
case "notifycliententerview":
// A client entered the server
nick := ""
if n, ok := args["client_nickname"]; ok {
nick = protocol.Unescape(n)
log.Printf("Client entered: %s", nick)
// If this matches our nickname, store the ClientID (Fallback if initserver missed)
if nick == c.Nickname && c.ClientID == 0 {
if clidStr, ok := args["clid"]; ok {
var id uint64
fmt.Sscanf(clidStr, "%d", &id)
c.ClientID = uint16(id)
log.Printf("Identified Self via notifycliententerview! ClientID: %d", c.ClientID)
}
}
}
case "notifytextmessage":
if msg, ok := args["msg"]; ok {
log.Printf("Text Message: %s", protocol.Unescape(msg))
}
case "notifychannelgrouplist":
// Ignore for now
case "notifyservergrouplist":
// Ignore for now
case "notifyclientneededpermissions":
// Ignore for now
case "notifyclientleftview":
if nick, ok := args["client_nickname"]; ok {
log.Printf("Client left: %s", protocol.Unescape(nick))
}
case "notifyconnectioninfo":
// Ignore
case "badges":
// Server badges info
case "notifyclientchatcomposing":
if nick, ok := args["client_nickname"]; ok {
// This often comes as clid, need to lookup in future
log.Printf("Client typing: %s", protocol.Unescape(nick))
}
case "notifyclientmoved":
if nick, ok := args["client_nickname"]; ok {
log.Printf("Client moved: %s", protocol.Unescape(nick))
}
case "error":
if id, ok := args["id"]; ok && id == "522" {
log.Println("WARNING: Server rejected client version (Error 522). Ignoring as requested.")
// We pretend we are connected?
// The server might not send further data, but we won't crash.
c.Connected = true
} else {
log.Printf("Server Error: %v", args)
}
default:
// Handle prefixes for weirdly updated commands
if strings.HasPrefix(cmd, "badges") {
// ignore badges garbage
log.Println("Received Badges (Ignored)")
return nil
}
// Fuzzy match for corrupted notifycliententerview
if strings.HasPrefix(cmd, "notifyclient") {
// Attempt to process it anyway
nick := ""
if n, ok := args["client_nickname"]; ok {
nick = protocol.Unescape(n)
log.Printf("Fuzzy Notify Client Entered: %s", nick)
if nick == c.Nickname && c.ClientID == 0 {
if clidStr, ok := args["clid"]; ok {
var id uint64
fmt.Sscanf(clidStr, "%d", &id)
c.ClientID = uint16(id)
log.Printf("Identified Self via Fuzzy Notify! ClientID: %d", c.ClientID)
}
}
}
return nil
}
// Log unknown commands for debugging
log.Printf("Unhandled command: %s Args: %v", cmd, args)
}
return nil
}
// Helper to encrypt/decrypt based on state
func (c *Client) getCryptoState() (key, nonce, mac []byte, isHandshake bool) {
if c.Handshake != nil && len(c.Handshake.SharedSecret) > 0 {
// Use Derived Keys
// But we need to Generate Key/Nonce per packet!
// This logic belongs in the Packet Encode/Decode flow or a higher level wrapper?
return nil, nil, c.Handshake.SharedMac, false
}
return protocol.HandshakeKey, protocol.HandshakeNonce, protocol.HandshakeMac[:], true
}
// Update encryption in Send/Receive
// Packet handling needs to know WHICH key to use.
// Simple rule:
// - Init1 (Type 8): Handshake Keys (Unencrypted payload, but MAC is HandshakeMac)
// - Command (Type 2): Encrypted.
// - CommandLow (Type 3): Encrypted.
// - Voice (Type 0): Encrypted.
// - Ping/Pong: Encrypted.
// - Ack: Encrypted.
// IF c.Handshake.SharedSecret is set, we SHOULD use it for Commands?
// "The crypto handshake is now completed. The normal encryption scheme ... is from now on used."
// This starts AFTER clientek? Or WITH clientek? "clientek already has the packet id 1"
func (c *Client) handleVoice(pkt *protocol.Packet) {
// Parse Voice Header
// 2 bytes VID, 1 byte Codec, Data
if len(pkt.Data) < 3 {
return
}
// vid := binary.BigEndian.Uint16(pkt.Data[0:2])
// codec := pkt.Data[2]
voiceData := pkt.Data[3:]
// Calculate "Volume" (RMS of encrypted/compressed data is meaningless, but existing requirement asks for it)
// To do this properly, we need to decrypt -> decode[Opus] -> PCM -> RMS.
// For "Eco" (Echo), we can just re-wrap this data and send it back.
vol := len(voiceData) // Placeholder "volume"
log.Printf("Voice Packet received. Approx Size/Vol: %d", vol)
// Echo back
// Client -> Server Voice Packet
// VID + Codec + Data
// We can reuse the data payload structure mostly?
// C->S: VID(2) + Codec(1) + Data
echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, pkt.Data)
// ID Counter handling?
c.Conn.SendPacket(echoPkt)
}
func (c *Client) sendClientInit() error {
// Build clientinit command
// Build clientinit command using TeamSpeak 3.6.2 credentials
params := map[string]string{
"client_nickname": c.Nickname,
"client_version": "3.6.2 [Build: 1690976575]",
"client_platform": "Windows",
"client_input_hardware": "1",
"client_output_hardware": "1",
"client_default_channel": "",
"client_default_channel_password": "",
"client_server_password": "",
"client_meta_data": "",
"client_version_sign": "OyuLO/1bVJtBsXLRWzfGVhNaQd7B9D4QTolZm14DM1uCbSXVvqX3Ssym3sLi/PcvOl+SAUlX6NwBPOsQdwOGDw==",
"client_key_offset": fmt.Sprintf("%d", c.Handshake.IdentityOffset),
"client_nickname_phonetic": "",
"client_default_token": "",
"hwid": "1234567890",
}
// Construct command string manually to ensure key correctness
var buf bytes.Buffer
buf.WriteString("clientinit")
for k, v := range params {
buf.WriteString(" ")
buf.WriteString(k)
buf.WriteString("=")
buf.WriteString(protocol.Escape(v))
}
cmd := buf.String()
pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd))
pkt.Header.PacketID = 2 // After clientek (1)
pkt.Header.Type |= protocol.PacketFlagNewProtocol
// After clientek, use SharedSecret encryption (Now that we fixed derivation logic)
crypto := &protocol.CryptoState{
SharedIV: c.Handshake.SharedIV,
SharedMac: c.Handshake.SharedMac,
GenerationID: 0,
}
// Client->Server = true
key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true)
// AAD must match the header structure exactly (excluding MAC)
// Client Header: PacketID (2) + ClientID (2) + Type|Flags (1)
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
// Byte 4 is Type (which includes Flags)
meta[4] = pkt.Header.Type
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
if err != nil {
return err
}
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
log.Println("Sending clientinit (Packet 2) [Encrypted with SharedSecret]...")
c.PacketIDCounterC2S = 2 // Update counter after clientinit
return c.Conn.SendPacket(pkt)
}