Files
go-ts/internal/client/send.go
Jose Luis Montañes Ojados 184fff202f Fix channel join flooding and enhance TUI features
- Implemented separate PacketID counters for Ping, Pong, and Ack (protocol compliance).
- Encrypted outgoing Pong packets after handshake.
- Fixed 'clientmove' command by omitting empty 'cpw' parameter.
- Added fullscreen log view toggle ('f' key).
- Improved logging with multi-writer and timestamps.
- Updated .gitignore to exclude binaries and logs.
2026-01-16 16:02:17 +01:00

148 lines
4.3 KiB
Go

package client
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"go-ts/pkg/protocol"
"github.com/dgryski/go-quicklz"
)
// SendCommand sends a command, splitting it into fragments if it exceeds 500 bytes.
func (c *Client) SendCommand(cmd *protocol.Command) error {
return c.SendCommandString(cmd.Encode())
}
// SendCommandString sends a raw command string with fragmentation.
func (c *Client) SendCommandString(cmdStr string) error {
log.Printf("Sending Command: %s", cmdStr)
data := []byte(cmdStr)
maxPacketSize := 500
maxBody := maxPacketSize - 13 // Header is 13 bytes for C->S (MAC 8, PID 2, TYPE 1, CID 2)
pType := protocol.PacketTypeCommand
pFlags := uint8(0)
// ts3j logic: If too large, try compressing
if len(data)+13 > maxPacketSize {
compressed := quicklz.Compress(data, 1)
if len(compressed)+13 < len(data)+13 {
data = compressed
pFlags |= protocol.PacketFlagCompressed
log.Printf("Compressed large command: %d -> %d bytes", len([]byte(cmdStr)), len(data))
}
}
// If still too large (or not compressible), fragment
if len(data)+13 > maxPacketSize {
log.Printf("Fragmenting large command (%d bytes) into %d packets", len(data), (len(data)/maxBody)+1)
for i := 0; i < len(data); i += maxBody {
end := i + maxBody
if end > len(data) {
end = len(data)
}
chunk := data[i:end]
chunkFlags := uint8(0)
// First packet keeps COMPRESSED flag (if set) and gets FRAGMENTED
if i == 0 {
chunkFlags = pFlags | protocol.PacketFlagFragmented
} else if end == len(data) {
// Last packet gets FRAGMENTED
chunkFlags = protocol.PacketFlagFragmented
} else {
// Intermediate packets have NO flags (other than NewProtocol added in sendPacketInternal)
chunkFlags = 0
}
if err := c.sendPacketInternal(chunk, pType, chunkFlags); err != nil {
return err
}
}
return nil
}
// Small enough to send in one go
return c.sendPacketInternal(data, pType, pFlags)
}
// sendPacketInternal handles encryption and low-level header construction for C->S packets.
func (c *Client) sendPacketInternal(data []byte, pType protocol.PacketType, flags uint8) error {
pkt := protocol.NewPacket(pType, data)
c.PacketIDCounterC2S++
pkt.Header.PacketID = c.PacketIDCounterC2S
pkt.Header.ClientID = c.ClientID
pkt.Header.Type |= protocol.PacketFlagNewProtocol | flags
// Encryption
// Use SharedSecret if Step >= 6, else fallback to HandshakeKey
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)
return c.Conn.SendPacket(pkt)
}
func (c *Client) sendClientInit() error {
// Specialized send for clientinit because it needs PID 2 and uses Map params
params := map[string]string{
"client_nickname": c.Nickname,
"client_version": "3.6.2 [Build: 1690976575]",
"client_platform": "Windows",
"client_input_muted": "0",
"client_output_muted": "0",
"client_outputonly_muted": "0",
"client_input_hardware": "1",
"client_output_hardware": "1",
"client_version_sign": "OyuLO/1bVJtBsXLRWzfGVhNaQd7B9D4QTolZm14DM1uCbSXVvqX3Ssym3sLi/PcvOl+SAUlX6NwBPOsQdwOGDw==",
"client_key_offset": fmt.Sprintf("%d", c.Handshake.IdentityOffset),
"hwid": "1234567890",
}
var buf bytes.Buffer
buf.WriteString("clientinit")
for k, v := range params {
buf.WriteString(" ")
buf.WriteString(k)
if v != "" {
buf.WriteString("=")
buf.WriteString(protocol.Escape(v))
}
}
log.Println("Sending clientinit (Packet 2) [Encrypted]...")
// Reset counter specifically for this sync point
c.PacketIDCounterC2S = 1 // Next will be 2
return c.SendCommandString(buf.String())
}