diff --git a/go.mod b/go.mod index 17bafa0..f06cfe5 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,8 @@ toolchain go1.24.11 require filippo.io/edwards25519 v1.1.0 -require golang.org/x/crypto v0.47.0 // indirect +require ( + github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0 // indirect + github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e // indirect + golang.org/x/crypto v0.47.0 // indirect +) diff --git a/go.sum b/go.sum index e1ea31c..3d4876f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0 h1:6+dYUy8Dg9WdPkXnA3MtdAZq1ry1+pLrc+ZMCMi0MPU= +github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0/go.mod h1:qBfAulKipfG2mar83JHqv6ykKbgDXHbmTWPY5gvr2zw= +github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e h1:MhBotBstN1h/GeA7lx7xstbFB8avummjt+nzOi2cY7Y= +github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e/go.mod h1:XLmYwGWgVzMPLlMmcNcWt3b5ixRabPLstWnPVEDRhzc= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= diff --git a/internal/client/client.go b/internal/client/client.go index 9906d51..43e87b4 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -10,6 +10,8 @@ import ( "go-ts/pkg/protocol" "go-ts/pkg/transport" + + "github.com/dgryski/go-quicklz" ) type Channel struct { @@ -32,6 +34,12 @@ type Client struct { // State Connected bool + // Fragment reassembly + FragmentBuffer []byte + FragmentStartPktID uint16 + FragmentCompressed bool + Fragmenting bool + // Server Data Channels map[uint64]*Channel } @@ -284,8 +292,73 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { 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 { @@ -345,6 +418,13 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { } } + 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) @@ -358,22 +438,17 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { pkt := protocol.NewPacket(protocol.PacketTypeCommand, []byte(cmd)) - // Encrypt - // key, nonce, mac, _ := c.getCryptoState() // Unused - - // Meta - meta := make([]byte, 5) - binary.BigEndian.PutUint16(meta[0:2], c.PacketIDCounterC2S+1) // Next ID + // 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++ - binary.BigEndian.PutUint16(meta[2:4], c.ClientID) - meta[4] = pkt.Header.Type - // TODO: Use correct crypto keys (SharedSecret if available) - // My getCryptoState returns correct ones? - // Let's manually do it to match sendClientInit logic for now or refactor later. - // Actually, if we are in Full Session, we should use SharedSecret. - // Handshake Step 6 -> SharedSecret. + // 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, @@ -386,12 +461,25 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { 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 - if nick, ok := args["client_nickname"]; ok { - log.Printf("Client entered: %s", protocol.Unescape(nick)) + 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 { @@ -436,8 +524,27 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { 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", cmd) + log.Printf("Unhandled command: %s Args: %v", cmd, args) } return nil @@ -559,5 +666,6 @@ func (c *Client) sendClientInit() error { 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) }