Files
go-ts/internal/client/client.go
Jose Luis Montañes Ojados 356b492629
All checks were successful
Build and Release / build-linux (push) Successful in 35s
Build and Release / build-windows (push) Successful in 2m35s
Build and Release / release (push) Has been skipped
Fix Poke timeout, input CommandLow handling, and add Poke Popup
2026-01-17 15:57:34 +01:00

228 lines
5.9 KiB
Go

package client
import (
"encoding/binary"
"fmt"
"log"
"sync"
"time"
"go-ts/pkg/protocol"
"go-ts/pkg/transport"
"gopkg.in/hraban/opus.v2"
)
type Channel struct {
ID uint64
ParentID uint64
Name string
Order uint64
}
// EventHandler is called when events occur
type EventHandler func(eventType string, data map[string]any)
type Client struct {
Conn *transport.TS3Conn
Handshake *HandshakeState
Nickname string
ClientID uint16
// Counters
PacketIDCounterC2S uint16 // Commands (Type 0x02)
VoicePacketID uint16 // Voice (Type 0x00)
PingPacketID uint16 // Type 0x04
PongPacketID uint16 // Type 0x05
AckPacketID uint16 // Type 0x06
AckLowPacketID uint16 // Type 0x07
// Ping RTT tracking
PingSentTimes map[uint16]time.Time // Map PingPacketID -> Time sent
PingRTT float64 // Rolling average RTT in ms
PingDeviation float64 // Rolling deviation in ms
PingSampleCount int // Number of samples for rolling avg
// State
Connected bool
ServerName string
// Fragment reassembly (packet queue like ts3j)
CommandQueue map[uint16]*protocol.Packet // Packets waiting for reassembly (Type 0x02)
ExpectedCommandPID uint16 // Next expected packet ID (Type 0x02)
FragmentState bool // Toggle: true = collecting, false = ready
CommandLowQueue map[uint16]*protocol.Packet // Packets waiting for reassembly (Type 0x03)
ExpectedCommandLowPID uint16 // Next expected packet ID (Type 0x03)
FragmentStateLow bool // Toggle: true = collecting, false = ready
// Server Data
Channels map[uint64]*Channel
// Audio
VoiceDecoders map[uint16]*opus.Decoder // Map VID (sender ID) to decoder
VoiceEncoder *opus.Encoder // Encoder for outgoing audio
VoiceEncoderMu sync.Mutex // Protects VoiceEncoder
// Event handler for public API
eventHandler EventHandler
// Done channel to signal shutdown
done chan struct{}
}
func NewClient(nickname string) *Client {
return &Client{
Nickname: nickname,
PacketIDCounterC2S: 1,
AckLowPacketID: 1,
VoicePacketID: 1,
Channels: make(map[uint64]*Channel),
VoiceDecoders: make(map[uint16]*opus.Decoder),
CommandQueue: make(map[uint16]*protocol.Packet),
ExpectedCommandPID: 0,
CommandLowQueue: make(map[uint16]*protocol.Packet),
ExpectedCommandLowPID: 0,
PingSentTimes: make(map[uint16]time.Time),
PingRTT: 0,
PingDeviation: 0,
done: make(chan struct{}),
}
}
// SetEventHandler sets the callback for events
func (c *Client) SetEventHandler(handler EventHandler) {
c.eventHandler = handler
}
// emitEvent sends an event to the handler if set
func (c *Client) emitEvent(eventType string, data map[string]any) {
if c.eventHandler != nil {
c.eventHandler(eventType, data)
}
}
// Stop signals the client to stop its loops
func (c *Client) Stop() {
select {
case <-c.done:
// Already closed
default:
close(c.done)
}
}
func (c *Client) Connect(address string) error {
conn, err := transport.NewTS3Conn(address)
if err != nil {
return err
}
c.Conn = conn
log.Printf("Connected to UDP. Starting Handshake...")
// 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)
// Send Init1
if err := c.Handshake.SendPacket0(); err != nil {
return err
}
// Listen Loop
pktChan := c.Conn.PacketChan()
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
// Recovery from panics in the main loop
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC in Client loop: %v", r)
}
}()
select {
case <-c.done:
log.Println("Client loop stopped")
return
case pkt := <-pktChan:
if pkt == nil {
// Channel closed
return
}
if err := c.handlePacket(pkt); err != nil {
log.Printf("Error handling packet: %v", err)
}
case <-ticker.C:
if !c.Connected {
return // Don't send pings if not connected yet
}
// Send KeepAlive Ping (Encrypted, No NewProtocol)
if err := c.sendPing(); err != nil {
log.Printf("Error sending Ping: %v", err)
}
}
}()
// Check if we should exit after the inner function
select {
case <-c.done:
return nil
default:
}
}
}
// sendPing sends an encrypted Ping packet WITHOUT the NewProtocol flag
func (c *Client) sendPing() error {
pType := protocol.PacketTypePing
pkt := protocol.NewPacket(pType, nil)
c.PingPacketID++
pkt.Header.PacketID = c.PingPacketID
pkt.Header.ClientID = c.ClientID
// Note: We do NOT set PacketFlagNewProtocol for Pings
// Encryption
key := protocol.HandshakeKey[:]
nonce := protocol.HandshakeNonce[:]
if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 {
crypto := &protocol.CryptoState{
SharedIV: c.Handshake.SharedIV,
SharedMac: c.Handshake.SharedMac,
GenerationID: 0,
}
keyArr, nonceArr := crypto.GenerateKeyNonce(&pkt.Header, true) // Client->Server=true
key = keyArr
nonce = nonceArr
}
// 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
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
if err != nil {
return fmt.Errorf("encryption failed: %w", err)
}
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
// Record send time for RTT calculation
c.PingSentTimes[pkt.Header.PacketID] = time.Now()
log.Printf("Sending proper Encrypted Ping (PID=%d)", pkt.Header.PacketID)
return c.Conn.SendPacket(pkt)
}