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 { 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()) }