| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897 |
- package icq_legacy
- import (
- "bytes"
- "encoding/binary"
- "fmt"
- "io"
- "net"
- "time"
- )
- // ICQ Legacy Protocol Packet Structures
- // Supports V2, V3, V4, and V5 protocols
- // LegacyPacketHeader contains common fields for all legacy packet versions
- type LegacyPacketHeader struct {
- Version uint16
- Command uint16
- SeqNum uint16
- }
- // V2ClientPacket represents an ICQ V2 client packet (unencrypted)
- // Format: Version(2) + Command(2) + SeqNum(2) + UIN(4) + [Parameters...]
- type V2ClientPacket struct {
- Version uint16
- Command uint16
- SeqNum uint16
- UIN uint32
- Data []byte // Remaining packet data
- }
- // V2ServerPacket represents an ICQ V2 server packet
- // Format: Version(2) + Command(2) + SeqNum(2) + [Parameters...]
- type V2ServerPacket struct {
- Version uint16
- Command uint16
- SeqNum uint16
- Data []byte // Packet payload
- }
- // V3ClientPacket represents an ICQ V3 client packet (with checksum)
- // Format: Version(2) + Command(2) + SeqNum(2) + UIN(4) + Checksum(4) + [Parameters...]
- type V3ClientPacket struct {
- Version uint16
- Command uint16
- SeqNum uint16
- UIN uint32
- Checksum uint32
- Data []byte
- }
- // V3ServerPacket represents an ICQ V3 server packet
- type V3ServerPacket struct {
- Version uint16
- Command uint16
- SeqNum uint16
- Checksum uint32
- Data []byte
- }
- // V4ClientPacket represents an ICQ V4 client packet (partially encrypted)
- // Format: Version(2) + Random(2) + [Encrypted: Zero(2) + Command(2) + SeqNum1(2) + SeqNum2(2) + UIN(4) + CheckCode(4) + Parameters...]
- type V4ClientPacket struct {
- Version uint16 // Not encrypted
- Random uint16 // Not encrypted
- Zero uint16 // Encrypted from here
- Command uint16
- SeqNum1 uint16
- SeqNum2 uint16
- UIN uint32
- CheckCode uint32
- Data []byte
- }
- // V4ServerPacket represents an ICQ V4 server packet
- type V4ServerPacket struct {
- Version uint16
- Random uint16
- Zero uint16
- Command uint16
- SeqNum1 uint16
- SeqNum2 uint16
- CheckCode uint32
- Data []byte
- }
- // V5ClientPacket represents an ICQ V5 client packet (fully encrypted)
- // Format: Version(2) + Zero(4) + UIN(4) + SessionID(4) + Command(2) + SeqNum1(2) + SeqNum2(2) + CheckCode(4) + [Parameters...]
- type V5ClientPacket struct {
- Version uint16
- Zero uint32
- UIN uint32
- SessionID uint32
- Command uint16
- SeqNum1 uint16
- SeqNum2 uint16
- CheckCode uint32
- Data []byte
- }
- // V5ServerPacket represents an ICQ V5 server packet
- // Server packet format (from iserverd):
- // VERSION(2) + ZERO(1) + SESSION_ID(4) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + JUNK(4) = 21 bytes
- // Checkcode is inserted at offset 0x11 (17) by PutKey
- type V5ServerPacket struct {
- Version uint16
- SessionID uint32
- Command uint16
- SeqNum1 uint16
- SeqNum2 uint16
- UIN uint32
- Data []byte
- }
- // V5MetaPacket represents a META_USER command packet (V5)
- type V5MetaPacket struct {
- SubCommand uint16
- Data []byte
- }
- // UnmarshalV2ClientPacket parses a V2 client packet from raw bytes
- // Supports both standard V2 format (10+ bytes with UIN in header) and
- // pre-V2/early V2 format (6+ bytes without UIN in header, used by 1996 clients)
- func UnmarshalV2ClientPacket(data []byte) (*V2ClientPacket, error) {
- if len(data) < 6 {
- return nil, fmt.Errorf("packet too short for V2: %d bytes", len(data))
- }
- pkt := &V2ClientPacket{
- Version: binary.LittleEndian.Uint16(data[0:2]),
- Command: binary.LittleEndian.Uint16(data[2:4]),
- SeqNum: binary.LittleEndian.Uint16(data[4:6]),
- }
- // Check if this is a pre-V2 packet format based on command type
- // Early 1996 clients use a 6-byte header (no UIN) for certain commands
- // Commands that use pre-V2 format (UIN in data, not header):
- // - 0x04EC (FIRST_LOGIN) - 6 byte header only
- // - 0x03F2 (early login) - UIN is in data payload
- // - 0x03FC (CMD_REG_NEW_USER) - registration uses srv_net_icq_pak (6-byte header, no UIN)
- isPreV2Command := pkt.Command == ICQLegacyCmdFirstLogin ||
- pkt.Command == ICQLegacyCmdGetDeps || // 0x03F2 - early login variant
- pkt.Command == ICQLegacyCmdRegNewUser // 0x03FC - registration (no UIN in header)
- if isPreV2Command {
- // Pre-V2 format: 6-byte header, UIN is in data payload (if present)
- if len(data) > 6 {
- pkt.Data = make([]byte, len(data)-6)
- copy(pkt.Data, data[6:])
- }
- } else if len(data) >= 10 {
- // Standard V2 format: UIN is in header at offset 6
- pkt.UIN = binary.LittleEndian.Uint32(data[6:10])
- if len(data) > 10 {
- pkt.Data = make([]byte, len(data)-10)
- copy(pkt.Data, data[10:])
- }
- } else if len(data) > 6 {
- // Short packet with some data but no full UIN - treat as pre-V2
- pkt.Data = make([]byte, len(data)-6)
- copy(pkt.Data, data[6:])
- }
- return pkt, nil
- }
- // MarshalV2ServerPacket serializes a V2 server packet to bytes
- func MarshalV2ServerPacket(pkt *V2ServerPacket) []byte {
- buf := make([]byte, 6+len(pkt.Data))
- binary.LittleEndian.PutUint16(buf[0:2], pkt.Version)
- binary.LittleEndian.PutUint16(buf[2:4], pkt.Command)
- binary.LittleEndian.PutUint16(buf[4:6], pkt.SeqNum)
- if len(pkt.Data) > 0 {
- copy(buf[6:], pkt.Data)
- }
- return buf
- }
- // UnmarshalV5ClientPacket parses a V5 client packet from raw bytes (after decryption)
- func UnmarshalV5ClientPacket(data []byte) (*V5ClientPacket, error) {
- if len(data) < 24 {
- return nil, fmt.Errorf("packet too short for V5: %d bytes", len(data))
- }
- pkt := &V5ClientPacket{
- Version: binary.LittleEndian.Uint16(data[0:2]),
- Zero: binary.LittleEndian.Uint32(data[2:6]),
- UIN: binary.LittleEndian.Uint32(data[6:10]),
- SessionID: binary.LittleEndian.Uint32(data[10:14]),
- Command: binary.LittleEndian.Uint16(data[14:16]),
- SeqNum1: binary.LittleEndian.Uint16(data[16:18]),
- SeqNum2: binary.LittleEndian.Uint16(data[18:20]),
- CheckCode: binary.LittleEndian.Uint32(data[20:24]),
- }
- if len(data) > 24 {
- pkt.Data = make([]byte, len(data)-24)
- copy(pkt.Data, data[24:])
- }
- return pkt, nil
- }
- // MarshalV5ServerPacket serializes a V5 server packet to bytes
- // Server packets are NOT encrypted, but have a checkcode added at offset 0x11
- // Format: VERSION(2) + ZERO(1) + SESSION_ID(4) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- //
- // CRITICAL: The "JUNK(4)" seen in iserverd source code (e.g., make_packet.cpp) refers to the
- // CHECKCODE placeholder at offset 0x11 in the HEADER - it is NOT part of the data payload!
- // The checkcode is computed over the packet and inserted by AddV5ServerCheckcode().
- // Do NOT add extra bytes to the data payload thinking they are "JUNK" - that breaks clients!
- func MarshalV5ServerPacket(pkt *V5ServerPacket) []byte {
- // Base packet without checkcode: 2 + 1 + 4 + 2 + 2 + 2 + 4 = 17 bytes
- // Then checkcode (4 bytes) + data
- buf := make([]byte, 21+len(pkt.Data))
- offset := 0
- // VERSION (2 bytes)
- binary.LittleEndian.PutUint16(buf[offset:], pkt.Version)
- offset += 2
- // ZERO (1 byte)
- buf[offset] = 0x00
- offset++
- // SESSION_ID (4 bytes)
- binary.LittleEndian.PutUint32(buf[offset:], pkt.SessionID)
- offset += 4
- // COMMAND (2 bytes)
- binary.LittleEndian.PutUint16(buf[offset:], pkt.Command)
- offset += 2
- // SEQ1 (2 bytes)
- binary.LittleEndian.PutUint16(buf[offset:], pkt.SeqNum1)
- offset += 2
- // SEQ2 (2 bytes)
- binary.LittleEndian.PutUint16(buf[offset:], pkt.SeqNum2)
- offset += 2
- // UIN (4 bytes)
- binary.LittleEndian.PutUint32(buf[offset:], pkt.UIN)
- offset += 4
- // CHECKCODE placeholder (4 bytes) - will be filled by AddV5ServerCheckcode
- // offset is now 17 (0x11)
- offset += 4
- // DATA
- if len(pkt.Data) > 0 {
- copy(buf[offset:], pkt.Data)
- }
- // Add checkcode
- AddV5ServerCheckcode(buf)
- return buf
- }
- // DetectProtocolVersion returns the protocol version from packet header
- func DetectProtocolVersion(data []byte) (uint16, error) {
- if len(data) < 2 {
- return 0, fmt.Errorf("packet too short to detect version")
- }
- return binary.LittleEndian.Uint16(data[0:2]), nil
- }
- // LegacyLoginPacket represents login data common across versions
- type LegacyLoginPacket struct {
- UIN uint32
- Password string
- IP net.IP
- Port uint16
- Status uint32
- Version uint32 // Client version
- ConnectionID uint16 // Internal connection ID for routing login reply
- }
- // LegacyMessagePacket represents a message packet
- type LegacyMessagePacket struct {
- FromUIN uint32
- ToUIN uint32
- MsgType uint16
- Message string
- URL string // For URL messages
- Desc string // URL description
- }
- // LegacyUserInfo represents user information in legacy format
- type LegacyUserInfo struct {
- UIN uint32
- Nickname string
- FirstName string
- LastName string
- Email string
- Auth uint8 // 0 = auth not required, 1 = auth required
- City string
- State string
- Country uint16
- Phone string
- Homepage string
- About string
- Age uint16
- Gender uint8
- Status uint32
- IP net.IP
- Port uint16
- }
- // LegacySearchRequest represents a user search request
- type LegacySearchRequest struct {
- UIN uint32 // For UIN search
- Nickname string
- FirstName string
- LastName string
- Email string
- }
- // LegacyContactListPacket represents a contact list update
- type LegacyContactListPacket struct {
- UINs []uint32
- }
- // LegacyStatusPacket represents a status change
- type LegacyStatusPacket struct {
- UIN uint32
- Status uint32
- IP net.IP
- Port uint16
- }
- // ParseLegacyString reads a null-terminated or length-prefixed string
- func ParseLegacyString(r io.Reader, lenPrefix bool) (string, error) {
- if lenPrefix {
- var length uint16
- if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
- return "", err
- }
- if length == 0 {
- return "", nil
- }
- buf := make([]byte, length)
- if _, err := io.ReadFull(r, buf); err != nil {
- return "", err
- }
- // Remove null terminator if present
- if len(buf) > 0 && buf[len(buf)-1] == 0 {
- buf = buf[:len(buf)-1]
- }
- return string(buf), nil
- }
- // Null-terminated string
- var buf bytes.Buffer
- b := make([]byte, 1)
- for {
- if _, err := r.Read(b); err != nil {
- return "", err
- }
- if b[0] == 0 {
- break
- }
- buf.WriteByte(b[0])
- }
- return buf.String(), nil
- }
- // WriteLegacyString writes a length-prefixed string
- func WriteLegacyString(w io.Writer, s string) error {
- // Include null terminator in length
- length := uint16(len(s) + 1)
- if err := binary.Write(w, binary.LittleEndian, length); err != nil {
- return err
- }
- if _, err := w.Write([]byte(s)); err != nil {
- return err
- }
- // Write null terminator
- _, err := w.Write([]byte{0})
- return err
- }
- // ParseV2LoginPacket parses a V2 login packet
- // V2 LOGIN format (verified against licq V2 client icqpacket.cpp CPU_Logon):
- //
- // After the 10-byte header (VER+CMD+SEQ+UIN), the data payload is:
- //
- // PORT(4) + PWD_LEN(2) + PASSWORD + X1(4) + USER_IP(4) + X2(1) +
- // STATUS(2) + DC_VERSION(2) + X3(12)
- //
- // Note: STATUS is 2 bytes (unsigned short), NOT 4 bytes. The client's
- // m_nLogonStatus is declared as unsigned short in icqpacket.h.
- // DC_VERSION follows immediately after status (also unsigned short).
- func ParseV2LoginPacket(data []byte) (*LegacyLoginPacket, error) {
- if len(data) < 15 { // Minimum: PORT(4) + PWD_LEN(2) + 1 char + null + X1(4) + IP(4)
- return nil, fmt.Errorf("login packet too short: %d bytes", len(data))
- }
- r := bytes.NewReader(data)
- pkt := &LegacyLoginPacket{}
- // Read TCP port (4 bytes, little-endian) - m_nLocalPort is unsigned long
- var port uint32
- if err := binary.Read(r, binary.LittleEndian, &port); err != nil {
- return nil, fmt.Errorf("parsing port: %w", err)
- }
- pkt.Port = uint16(port)
- // Read password (length-prefixed string)
- password, err := ParseLegacyString(r, true)
- if err != nil {
- return nil, fmt.Errorf("parsing password: %w", err)
- }
- pkt.Password = password
- // Read X1 (4 bytes, usually { 0x72, 0x00, 0x04, 0x00 })
- var x1 uint32
- if err := binary.Read(r, binary.LittleEndian, &x1); err != nil {
- return nil, fmt.Errorf("parsing X1: %w", err)
- }
- // Read IP (4 bytes) - m_nLocalIP, stored in packet byte order
- ip := make([]byte, 4)
- if _, err := io.ReadFull(r, ip); err != nil {
- return nil, fmt.Errorf("parsing IP: %w", err)
- }
- pkt.IP = net.IP(ip)
- // Read X2 (1 byte, usually 0x04) - m_aUnknown_2
- var x2 uint8
- if err := binary.Read(r, binary.LittleEndian, &x2); err != nil {
- return nil, fmt.Errorf("parsing X2: %w", err)
- }
- // Read status (2 bytes) - m_nLogonStatus is unsigned short
- var status uint16
- if err := binary.Read(r, binary.LittleEndian, &status); err != nil {
- return nil, fmt.Errorf("parsing status: %w", err)
- }
- pkt.Status = uint32(status)
- // Read DC version (2 bytes) - m_nTcpVersion is unsigned short
- // TODO: store dcVersion in LegacyLoginPacket so V2 handler can pass it
- // to SetDirectConnectionInfo when direct connections are enabled for V2.
- var dcVersion uint16
- if err := binary.Read(r, binary.LittleEndian, &dcVersion); err != nil {
- // Optional field
- return pkt, nil
- }
- // Read X3 (12 bytes) - m_aUnknown_3
- // Byte 8-9 of X3 contains the internal connection ID used for routing
- // the login reply back to the correct wizard page (from sub_46C0B6 disasm).
- x3 := make([]byte, 12)
- if _, err := io.ReadFull(r, x3); err != nil {
- // Optional trailing data
- return pkt, nil
- }
- if len(x3) >= 10 {
- pkt.ConnectionID = binary.LittleEndian.Uint16(x3[8:10])
- }
- return pkt, nil
- }
- // BuildV2LoginReply creates a login success response
- // V2 LOGIN_REPLY format (from protocol spec):
- // USER_UIN(4) + USER_IP(4) + LOGIN_SEQ_NUM(2) + X1(4) + X2(4) + X3(4) + X4(4) + X5(7) = 33 bytes
- // Format from real ICQ server capture (licq source):
- // 8F 76 20 00 CD CD 76 10 02 00 01 00 05 00 00 00 00 00 8C 00 00 00 F0 00 0A 00 0A 00 05 00 0A 00 01
- func BuildV2LoginReply(serverSeqNum uint16, clientConnectionID uint16, uin uint32, clientIP net.IP) *V2ServerPacket {
- data := make([]byte, 33)
- offset := 0
- // USER_UIN (4 bytes)
- binary.LittleEndian.PutUint32(data[offset:], uin)
- offset += 4
- // USER_IP (4 bytes) - server's view of client IP
- if clientIP != nil {
- ip4 := clientIP.To4()
- if ip4 != nil {
- copy(data[offset:offset+4], ip4)
- }
- }
- offset += 4
- // LOGIN_SEQ_NUM (2 bytes)
- binary.LittleEndian.PutUint16(data[offset:], clientConnectionID)
- offset += 2
- // X1 (4 bytes): 01 00 05 00
- data[offset] = 0x01
- data[offset+1] = 0x00
- data[offset+2] = 0x05
- data[offset+3] = 0x00
- offset += 4
- // X2 (4 bytes): 00 00 00 00
- offset += 4
- // X3 (4 bytes): 8C 00 00 00
- data[offset] = 0x8C
- offset += 4
- // X4 (4 bytes): F0 00 0A 00
- data[offset] = 0xF0
- data[offset+1] = 0x00
- data[offset+2] = 0x0A
- data[offset+3] = 0x00
- offset += 4
- // X5 (7 bytes): 0A 00 05 00 0A 00 01
- data[offset] = 0x0A
- data[offset+1] = 0x00
- data[offset+2] = 0x05
- data[offset+3] = 0x00
- data[offset+4] = 0x0A
- data[offset+5] = 0x00
- data[offset+6] = 0x01
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvHello,
- SeqNum: serverSeqNum,
- Data: data,
- }
- }
- // BuildV2BadPassword creates a bad password response
- func BuildV2BadPassword(seqNum uint16) *V2ServerPacket {
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvWrongPasswd,
- SeqNum: seqNum,
- }
- }
- // BuildV2UserOnline creates a user online notification
- // V2 USER_ONLINE format (verified against licq V2 client icq-udp.h):
- //
- // The client reads:
- //
- // UIN(4) + IP(4) + PORT(2) + JUNK(4+2+1) + STATUS(2) + TRAILING(4+2)
- //
- // Total = 25 bytes
- //
- // Field breakdown matching the client's read order:
- //
- // REMOTE_UIN(4) + REMOTE_IP(4) + REMOTE_PORT(2) + REMOTE_REAL_IP(4) +
- // DC_VERSION(2) + X1(1) + STATUS(2) + X2(4) + X3(2) = 25 bytes
- //
- // The client treats bytes 10-16 as "junk" (it reads them into junkLong,
- // junkShort, junkChar but discards the values). STATUS must be at offset 17
- // as a uint16 LE. PORT must be at offset 8 as a uint16 LE.
- func BuildV2UserOnline(seqNum uint16, uin uint32, status uint32, ip net.IP, port uint16) *V2ServerPacket {
- data := make([]byte, 25)
- offset := 0
- // REMOTE_UIN (4 bytes)
- binary.LittleEndian.PutUint32(data[offset:], uin)
- offset += 4
- // REMOTE_IP (4 bytes) - "outer" IP address
- if ip != nil {
- ip4 := ip.To4()
- if ip4 != nil {
- copy(data[offset:offset+4], ip4)
- }
- }
- offset += 4
- // REMOTE_PORT (2 bytes, little-endian) - TCP port for direct connection
- // Client reads this as unsigned short (2 bytes)
- binary.LittleEndian.PutUint16(data[offset:], port)
- offset += 2
- // Next 7 bytes are read by client as junkLong(4) + junkShort(2) + junkChar(1)
- // but discarded. We fill them with meaningful data matching the original
- // Mirabilis server format: REAL_IP(4) + DC_VERSION(2) + X1(1)
- // REMOTE_REAL_IP (4 bytes) - "inner" IP address
- if ip != nil {
- ip4 := ip.To4()
- if ip4 != nil {
- copy(data[offset:offset+4], ip4)
- }
- }
- offset += 4
- // DC_VERSION (2 bytes) - client's DC version (junkShort)
- binary.LittleEndian.PutUint16(data[offset:], 0x0003)
- offset += 2
- // X1 (1 byte): 0x04 (junkChar)
- data[offset] = 0x04
- offset++
- // STATUS (2 bytes, little-endian) - client reads as unsigned short
- binary.LittleEndian.PutUint16(data[offset:], uint16(status))
- offset += 2
- // X2 (4 bytes): trailing data
- data[offset] = 0x02
- data[offset+1] = 0x00
- data[offset+2] = 0x00
- data[offset+3] = 0x00
- offset += 4
- // X3 (2 bytes): more trailing data (total = 25 bytes)
- data[offset] = 0x00
- data[offset+1] = 0x00
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvUserOnline,
- SeqNum: seqNum,
- Data: data,
- }
- }
- // BuildV2UserOffline creates a user offline notification
- func BuildV2UserOffline(seqNum uint16, uin uint32) *V2ServerPacket {
- data := make([]byte, 4)
- binary.LittleEndian.PutUint32(data[0:4], uin)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvUserOffline,
- SeqNum: seqNum,
- Data: data,
- }
- }
- // BuildV2Ack creates an acknowledgment packet
- func BuildV2Ack(seqNum uint16) *V2ServerPacket {
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvAck,
- SeqNum: seqNum,
- }
- }
- // BuildV2OfflineMessage creates an offline message delivery packet (SRV_SYS_MSG_OFFLINE 0x00DC).
- // The client (licq icq-udp.h case ICQ_CMDxRCV_SYSxMSGxOFFLINE) expects:
- //
- // UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MIN(1) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- //
- // This preserves the original message timestamp, unlike SRV_SYS_MSG_ONLINE (0x0104)
- // which the client treats as "received now".
- func BuildV2OfflineMessage(seqNum uint16, fromUIN uint32, msgType uint16, message string, timestamp time.Time) *V2ServerPacket {
- buf := new(bytes.Buffer)
- _ = binary.Write(buf, binary.LittleEndian, fromUIN)
- _ = binary.Write(buf, binary.LittleEndian, uint16(timestamp.Year()))
- buf.WriteByte(byte(timestamp.Month()))
- buf.WriteByte(byte(timestamp.Day()))
- buf.WriteByte(byte(timestamp.Hour()))
- buf.WriteByte(byte(timestamp.Minute()))
- _ = binary.Write(buf, binary.LittleEndian, msgType)
- _ = WriteLegacyString(buf, message)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvSysMsgOffline,
- SeqNum: seqNum,
- Data: buf.Bytes(),
- }
- }
- // BuildV2Message creates a message delivery packet
- func BuildV2Message(seqNum uint16, fromUIN uint32, msgType uint16, message string) *V2ServerPacket {
- buf := new(bytes.Buffer)
- _ = binary.Write(buf, binary.LittleEndian, fromUIN)
- _ = binary.Write(buf, binary.LittleEndian, msgType)
- _ = WriteLegacyString(buf, message)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvSysMsgOnline,
- SeqNum: seqNum,
- Data: buf.Bytes(),
- }
- }
- // BuildV2StatusUpdate creates a status update notification
- func BuildV2StatusUpdate(seqNum uint16, uin uint32, status uint32) *V2ServerPacket {
- data := make([]byte, 8)
- binary.LittleEndian.PutUint32(data[0:4], uin)
- binary.LittleEndian.PutUint32(data[4:8], status)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvUserStatus,
- SeqNum: seqNum,
- Data: data,
- }
- }
- // BuildV2ContactListDone creates a contact list processed response
- // V2 format: UIN(4) — from licq example: 02 00 1C 02 05 00 8F 76 20 00
- func BuildV2ContactListDone(seqNum uint16, uin uint32) *V2ServerPacket {
- data := make([]byte, 4)
- binary.LittleEndian.PutUint32(data[0:4], uin)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvUserListDone,
- SeqNum: seqNum,
- Data: data,
- }
- }
- // BuildV2SearchResult creates a search result packet
- // V2 format (from center-1.10.7 icq_HandleSearchReply / icq_HandleInfoReply):
- // V2 search found format (from licq):
- // SEQ(2) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
- // V2 search done format (from licq): SEQ(2) + MORE(1)
- func BuildV2SearchResult(seqNum uint16, user *LegacyUserInfo, isLast bool) *V2ServerPacket {
- buf := new(bytes.Buffer)
- _ = binary.Write(buf, binary.LittleEndian, seqNum)
- if user.UIN == 0 && isLast {
- // No results — just send search done with more=0
- buf.WriteByte(0x00)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvSearchDone,
- SeqNum: seqNum,
- Data: buf.Bytes(),
- }
- }
- // Send search found with user data
- _ = binary.Write(buf, binary.LittleEndian, user.UIN)
- _ = WriteLegacyString(buf, user.Nickname)
- _ = WriteLegacyString(buf, user.FirstName)
- _ = WriteLegacyString(buf, user.LastName)
- _ = WriteLegacyString(buf, user.Email)
- buf.WriteByte(user.Auth)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvSearchFound,
- SeqNum: seqNum,
- Data: buf.Bytes(),
- }
- }
- // BuildV2InfoReply creates a basic info reply packet (SRV_INFO_REPLY 0x0118)
- // V2 format (from center-1.10.7 icq_HandleInfoReply):
- // SEQ(2) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
- // Same payload format as search result, but uses command 0x0118
- func BuildV2InfoReply(serverSeq uint16, checkSeq uint16, user *LegacyUserInfo) *V2ServerPacket {
- buf := new(bytes.Buffer)
- // checkSequence - echoes the client's info sub-sequence so DoneExtendedEvent can match
- _ = binary.Write(buf, binary.LittleEndian, checkSeq)
- _ = binary.Write(buf, binary.LittleEndian, user.UIN)
- _ = WriteLegacyString(buf, user.Nickname)
- _ = WriteLegacyString(buf, user.FirstName)
- _ = WriteLegacyString(buf, user.LastName)
- _ = WriteLegacyString(buf, user.Email)
- // AUTH byte (0 = auth not required, 1 = auth required)
- buf.WriteByte(user.Auth)
- // Trailing padding (observed in licq example packets)
- _ = binary.Write(buf, binary.LittleEndian, uint16(0))
- _ = binary.Write(buf, binary.LittleEndian, uint16(0))
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvInfoReply,
- SeqNum: serverSeq,
- Data: buf.Bytes(),
- }
- }
- // BuildV2ExtInfoReply creates an extended info reply packet (SRV_EXT_INFO_REPLY 0x0122)
- // V2 format (from center-1.10.7 icq_HandleExtInfoReply):
- // SEQ(2) + UIN(4) + CITY_LEN(2) + CITY + COUNTRY(2) + COUNTRY_STAT(1) + STATE_LEN(2) + STATE +
- // AGE(2) + GENDER(1) + PHONE_LEN(2) + PHONE + HP_LEN(2) + HP + ABOUT_LEN(2) + ABOUT
- func BuildV2ExtInfoReply(serverSeq uint16, checkSeq uint16, user *LegacyUserInfo) *V2ServerPacket {
- buf := new(bytes.Buffer)
- // checkSequence - echoes the client's info sub-sequence
- _ = binary.Write(buf, binary.LittleEndian, checkSeq)
- // UIN
- _ = binary.Write(buf, binary.LittleEndian, user.UIN)
- // City (length-prefixed string)
- _ = WriteLegacyString(buf, user.City)
- // Country code (2 bytes)
- _ = binary.Write(buf, binary.LittleEndian, user.Country)
- // Country stat (1 byte) - 0 = not specified
- buf.WriteByte(0)
- // State (length-prefixed string)
- _ = WriteLegacyString(buf, user.State)
- // Age (2 bytes)
- _ = binary.Write(buf, binary.LittleEndian, user.Age)
- // Gender (1 byte)
- buf.WriteByte(user.Gender)
- // Phone (length-prefixed string)
- _ = WriteLegacyString(buf, user.Phone)
- // Homepage (length-prefixed string)
- _ = WriteLegacyString(buf, user.Homepage)
- // About (length-prefixed string)
- _ = WriteLegacyString(buf, user.About)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvExtInfoReply,
- SeqNum: serverSeq,
- Data: buf.Bytes(),
- }
- }
- // BuildV2NewUIN creates a SRV_NEW_UIN (0x0046) response for registration.
- // The client (center icq_InitNewUser) reads the new UIN from pak.data[2]:
- //
- // icq_Uin = Chars_2_DW(&pak.data[2])
- //
- // So the data format is: SEQ(2) + UIN(4)
- // The server packet uses the srv_net_icq_pak 6-byte header (ver+cmd+seq).
- func BuildV2NewUIN(seqNum uint16, newUIN uint32) *V2ServerPacket {
- data := make([]byte, 6)
- binary.LittleEndian.PutUint16(data[0:2], seqNum)
- binary.LittleEndian.PutUint32(data[2:6], newUIN)
- return &V2ServerPacket{
- Version: ICQLegacyVersionV2,
- Command: ICQLegacySrvNewUIN,
- SeqNum: seqNum,
- Data: data,
- }
- }
- // ParseV2ContactList parses a contact list packet
- func ParseV2ContactList(data []byte) (*LegacyContactListPacket, error) {
- if len(data) < 1 {
- return nil, fmt.Errorf("contact list packet too short")
- }
- r := bytes.NewReader(data)
- var count uint8
- if err := binary.Read(r, binary.LittleEndian, &count); err != nil {
- return nil, err
- }
- pkt := &LegacyContactListPacket{
- UINs: make([]uint32, 0, count),
- }
- for i := uint8(0); i < count; i++ {
- var uin uint32
- if err := binary.Read(r, binary.LittleEndian, &uin); err != nil {
- break // May have fewer UINs than count
- }
- pkt.UINs = append(pkt.UINs, uin)
- }
- return pkt, nil
- }
- // ParseV2Message parses a message packet
- func ParseV2Message(data []byte) (*LegacyMessagePacket, error) {
- if len(data) < 6 {
- return nil, fmt.Errorf("message packet too short")
- }
- r := bytes.NewReader(data)
- pkt := &LegacyMessagePacket{}
- if err := binary.Read(r, binary.LittleEndian, &pkt.ToUIN); err != nil {
- return nil, err
- }
- if err := binary.Read(r, binary.LittleEndian, &pkt.MsgType); err != nil {
- return nil, err
- }
- msg, err := ParseLegacyString(r, true)
- if err != nil {
- return nil, err
- }
- pkt.Message = msg
- // For URL messages, parse description
- if pkt.MsgType == ICQLegacyMsgURL {
- // URL format: "description\xFEurl"
- parts := bytes.SplitN([]byte(pkt.Message), []byte{0xFE}, 2)
- if len(parts) == 2 {
- pkt.Desc = string(parts[0])
- pkt.URL = string(parts[1])
- } else {
- pkt.URL = pkt.Message
- }
- }
- return pkt, nil
- }
|