wire_legacy.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. package icq_legacy
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "fmt"
  6. "io"
  7. "net"
  8. "time"
  9. )
  10. // ICQ Legacy Protocol Packet Structures
  11. // Supports V2, V3, V4, and V5 protocols
  12. // LegacyPacketHeader contains common fields for all legacy packet versions
  13. type LegacyPacketHeader struct {
  14. Version uint16
  15. Command uint16
  16. SeqNum uint16
  17. }
  18. // V2ClientPacket represents an ICQ V2 client packet (unencrypted)
  19. // Format: Version(2) + Command(2) + SeqNum(2) + UIN(4) + [Parameters...]
  20. type V2ClientPacket struct {
  21. Version uint16
  22. Command uint16
  23. SeqNum uint16
  24. UIN uint32
  25. Data []byte // Remaining packet data
  26. }
  27. // V2ServerPacket represents an ICQ V2 server packet
  28. // Format: Version(2) + Command(2) + SeqNum(2) + [Parameters...]
  29. type V2ServerPacket struct {
  30. Version uint16
  31. Command uint16
  32. SeqNum uint16
  33. Data []byte // Packet payload
  34. }
  35. // V3ClientPacket represents an ICQ V3 client packet (with checksum)
  36. // Format: Version(2) + Command(2) + SeqNum(2) + UIN(4) + Checksum(4) + [Parameters...]
  37. type V3ClientPacket struct {
  38. Version uint16
  39. Command uint16
  40. SeqNum uint16
  41. UIN uint32
  42. Checksum uint32
  43. Data []byte
  44. }
  45. // V3ServerPacket represents an ICQ V3 server packet
  46. type V3ServerPacket struct {
  47. Version uint16
  48. Command uint16
  49. SeqNum uint16
  50. Checksum uint32
  51. Data []byte
  52. }
  53. // V4ClientPacket represents an ICQ V4 client packet (partially encrypted)
  54. // Format: Version(2) + Random(2) + [Encrypted: Zero(2) + Command(2) + SeqNum1(2) + SeqNum2(2) + UIN(4) + CheckCode(4) + Parameters...]
  55. type V4ClientPacket struct {
  56. Version uint16 // Not encrypted
  57. Random uint16 // Not encrypted
  58. Zero uint16 // Encrypted from here
  59. Command uint16
  60. SeqNum1 uint16
  61. SeqNum2 uint16
  62. UIN uint32
  63. CheckCode uint32
  64. Data []byte
  65. }
  66. // V4ServerPacket represents an ICQ V4 server packet
  67. type V4ServerPacket struct {
  68. Version uint16
  69. Random uint16
  70. Zero uint16
  71. Command uint16
  72. SeqNum1 uint16
  73. SeqNum2 uint16
  74. CheckCode uint32
  75. Data []byte
  76. }
  77. // V5ClientPacket represents an ICQ V5 client packet (fully encrypted)
  78. // Format: Version(2) + Zero(4) + UIN(4) + SessionID(4) + Command(2) + SeqNum1(2) + SeqNum2(2) + CheckCode(4) + [Parameters...]
  79. type V5ClientPacket struct {
  80. Version uint16
  81. Zero uint32
  82. UIN uint32
  83. SessionID uint32
  84. Command uint16
  85. SeqNum1 uint16
  86. SeqNum2 uint16
  87. CheckCode uint32
  88. Data []byte
  89. }
  90. // V5ServerPacket represents an ICQ V5 server packet
  91. // Server packet format (from iserverd):
  92. // VERSION(2) + ZERO(1) + SESSION_ID(4) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + JUNK(4) = 21 bytes
  93. // Checkcode is inserted at offset 0x11 (17) by PutKey
  94. type V5ServerPacket struct {
  95. Version uint16
  96. SessionID uint32
  97. Command uint16
  98. SeqNum1 uint16
  99. SeqNum2 uint16
  100. UIN uint32
  101. Data []byte
  102. }
  103. // V5MetaPacket represents a META_USER command packet (V5)
  104. type V5MetaPacket struct {
  105. SubCommand uint16
  106. Data []byte
  107. }
  108. // UnmarshalV2ClientPacket parses a V2 client packet from raw bytes
  109. // Supports both standard V2 format (10+ bytes with UIN in header) and
  110. // pre-V2/early V2 format (6+ bytes without UIN in header, used by 1996 clients)
  111. func UnmarshalV2ClientPacket(data []byte) (*V2ClientPacket, error) {
  112. if len(data) < 6 {
  113. return nil, fmt.Errorf("packet too short for V2: %d bytes", len(data))
  114. }
  115. pkt := &V2ClientPacket{
  116. Version: binary.LittleEndian.Uint16(data[0:2]),
  117. Command: binary.LittleEndian.Uint16(data[2:4]),
  118. SeqNum: binary.LittleEndian.Uint16(data[4:6]),
  119. }
  120. // Check if this is a pre-V2 packet format based on command type
  121. // Early 1996 clients use a 6-byte header (no UIN) for certain commands
  122. // Commands that use pre-V2 format (UIN in data, not header):
  123. // - 0x04EC (FIRST_LOGIN) - 6 byte header only
  124. // - 0x03F2 (early login) - UIN is in data payload
  125. // - 0x03FC (CMD_REG_NEW_USER) - registration uses srv_net_icq_pak (6-byte header, no UIN)
  126. isPreV2Command := pkt.Command == ICQLegacyCmdFirstLogin ||
  127. pkt.Command == ICQLegacyCmdGetDeps || // 0x03F2 - early login variant
  128. pkt.Command == ICQLegacyCmdRegNewUser // 0x03FC - registration (no UIN in header)
  129. if isPreV2Command {
  130. // Pre-V2 format: 6-byte header, UIN is in data payload (if present)
  131. if len(data) > 6 {
  132. pkt.Data = make([]byte, len(data)-6)
  133. copy(pkt.Data, data[6:])
  134. }
  135. } else if len(data) >= 10 {
  136. // Standard V2 format: UIN is in header at offset 6
  137. pkt.UIN = binary.LittleEndian.Uint32(data[6:10])
  138. if len(data) > 10 {
  139. pkt.Data = make([]byte, len(data)-10)
  140. copy(pkt.Data, data[10:])
  141. }
  142. } else if len(data) > 6 {
  143. // Short packet with some data but no full UIN - treat as pre-V2
  144. pkt.Data = make([]byte, len(data)-6)
  145. copy(pkt.Data, data[6:])
  146. }
  147. return pkt, nil
  148. }
  149. // MarshalV2ServerPacket serializes a V2 server packet to bytes
  150. func MarshalV2ServerPacket(pkt *V2ServerPacket) []byte {
  151. buf := make([]byte, 6+len(pkt.Data))
  152. binary.LittleEndian.PutUint16(buf[0:2], pkt.Version)
  153. binary.LittleEndian.PutUint16(buf[2:4], pkt.Command)
  154. binary.LittleEndian.PutUint16(buf[4:6], pkt.SeqNum)
  155. if len(pkt.Data) > 0 {
  156. copy(buf[6:], pkt.Data)
  157. }
  158. return buf
  159. }
  160. // UnmarshalV5ClientPacket parses a V5 client packet from raw bytes (after decryption)
  161. func UnmarshalV5ClientPacket(data []byte) (*V5ClientPacket, error) {
  162. if len(data) < 24 {
  163. return nil, fmt.Errorf("packet too short for V5: %d bytes", len(data))
  164. }
  165. pkt := &V5ClientPacket{
  166. Version: binary.LittleEndian.Uint16(data[0:2]),
  167. Zero: binary.LittleEndian.Uint32(data[2:6]),
  168. UIN: binary.LittleEndian.Uint32(data[6:10]),
  169. SessionID: binary.LittleEndian.Uint32(data[10:14]),
  170. Command: binary.LittleEndian.Uint16(data[14:16]),
  171. SeqNum1: binary.LittleEndian.Uint16(data[16:18]),
  172. SeqNum2: binary.LittleEndian.Uint16(data[18:20]),
  173. CheckCode: binary.LittleEndian.Uint32(data[20:24]),
  174. }
  175. if len(data) > 24 {
  176. pkt.Data = make([]byte, len(data)-24)
  177. copy(pkt.Data, data[24:])
  178. }
  179. return pkt, nil
  180. }
  181. // MarshalV5ServerPacket serializes a V5 server packet to bytes
  182. // Server packets are NOT encrypted, but have a checkcode added at offset 0x11
  183. // Format: VERSION(2) + ZERO(1) + SESSION_ID(4) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
  184. //
  185. // CRITICAL: The "JUNK(4)" seen in iserverd source code (e.g., make_packet.cpp) refers to the
  186. // CHECKCODE placeholder at offset 0x11 in the HEADER - it is NOT part of the data payload!
  187. // The checkcode is computed over the packet and inserted by AddV5ServerCheckcode().
  188. // Do NOT add extra bytes to the data payload thinking they are "JUNK" - that breaks clients!
  189. func MarshalV5ServerPacket(pkt *V5ServerPacket) []byte {
  190. // Base packet without checkcode: 2 + 1 + 4 + 2 + 2 + 2 + 4 = 17 bytes
  191. // Then checkcode (4 bytes) + data
  192. buf := make([]byte, 21+len(pkt.Data))
  193. offset := 0
  194. // VERSION (2 bytes)
  195. binary.LittleEndian.PutUint16(buf[offset:], pkt.Version)
  196. offset += 2
  197. // ZERO (1 byte)
  198. buf[offset] = 0x00
  199. offset++
  200. // SESSION_ID (4 bytes)
  201. binary.LittleEndian.PutUint32(buf[offset:], pkt.SessionID)
  202. offset += 4
  203. // COMMAND (2 bytes)
  204. binary.LittleEndian.PutUint16(buf[offset:], pkt.Command)
  205. offset += 2
  206. // SEQ1 (2 bytes)
  207. binary.LittleEndian.PutUint16(buf[offset:], pkt.SeqNum1)
  208. offset += 2
  209. // SEQ2 (2 bytes)
  210. binary.LittleEndian.PutUint16(buf[offset:], pkt.SeqNum2)
  211. offset += 2
  212. // UIN (4 bytes)
  213. binary.LittleEndian.PutUint32(buf[offset:], pkt.UIN)
  214. offset += 4
  215. // CHECKCODE placeholder (4 bytes) - will be filled by AddV5ServerCheckcode
  216. // offset is now 17 (0x11)
  217. offset += 4
  218. // DATA
  219. if len(pkt.Data) > 0 {
  220. copy(buf[offset:], pkt.Data)
  221. }
  222. // Add checkcode
  223. AddV5ServerCheckcode(buf)
  224. return buf
  225. }
  226. // DetectProtocolVersion returns the protocol version from packet header
  227. func DetectProtocolVersion(data []byte) (uint16, error) {
  228. if len(data) < 2 {
  229. return 0, fmt.Errorf("packet too short to detect version")
  230. }
  231. return binary.LittleEndian.Uint16(data[0:2]), nil
  232. }
  233. // LegacyLoginPacket represents login data common across versions
  234. type LegacyLoginPacket struct {
  235. UIN uint32
  236. Password string
  237. IP net.IP
  238. Port uint16
  239. Status uint32
  240. Version uint32 // Client version
  241. ConnectionID uint16 // Internal connection ID for routing login reply
  242. }
  243. // LegacyMessagePacket represents a message packet
  244. type LegacyMessagePacket struct {
  245. FromUIN uint32
  246. ToUIN uint32
  247. MsgType uint16
  248. Message string
  249. URL string // For URL messages
  250. Desc string // URL description
  251. }
  252. // LegacyUserInfo represents user information in legacy format
  253. type LegacyUserInfo struct {
  254. UIN uint32
  255. Nickname string
  256. FirstName string
  257. LastName string
  258. Email string
  259. Auth uint8 // 0 = auth not required, 1 = auth required
  260. City string
  261. State string
  262. Country uint16
  263. Phone string
  264. Homepage string
  265. About string
  266. Age uint16
  267. Gender uint8
  268. Status uint32
  269. IP net.IP
  270. Port uint16
  271. }
  272. // LegacySearchRequest represents a user search request
  273. type LegacySearchRequest struct {
  274. UIN uint32 // For UIN search
  275. Nickname string
  276. FirstName string
  277. LastName string
  278. Email string
  279. }
  280. // LegacyContactListPacket represents a contact list update
  281. type LegacyContactListPacket struct {
  282. UINs []uint32
  283. }
  284. // LegacyStatusPacket represents a status change
  285. type LegacyStatusPacket struct {
  286. UIN uint32
  287. Status uint32
  288. IP net.IP
  289. Port uint16
  290. }
  291. // ParseLegacyString reads a null-terminated or length-prefixed string
  292. func ParseLegacyString(r io.Reader, lenPrefix bool) (string, error) {
  293. if lenPrefix {
  294. var length uint16
  295. if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
  296. return "", err
  297. }
  298. if length == 0 {
  299. return "", nil
  300. }
  301. buf := make([]byte, length)
  302. if _, err := io.ReadFull(r, buf); err != nil {
  303. return "", err
  304. }
  305. // Remove null terminator if present
  306. if len(buf) > 0 && buf[len(buf)-1] == 0 {
  307. buf = buf[:len(buf)-1]
  308. }
  309. return string(buf), nil
  310. }
  311. // Null-terminated string
  312. var buf bytes.Buffer
  313. b := make([]byte, 1)
  314. for {
  315. if _, err := r.Read(b); err != nil {
  316. return "", err
  317. }
  318. if b[0] == 0 {
  319. break
  320. }
  321. buf.WriteByte(b[0])
  322. }
  323. return buf.String(), nil
  324. }
  325. // WriteLegacyString writes a length-prefixed string
  326. func WriteLegacyString(w io.Writer, s string) error {
  327. // Include null terminator in length
  328. length := uint16(len(s) + 1)
  329. if err := binary.Write(w, binary.LittleEndian, length); err != nil {
  330. return err
  331. }
  332. if _, err := w.Write([]byte(s)); err != nil {
  333. return err
  334. }
  335. // Write null terminator
  336. _, err := w.Write([]byte{0})
  337. return err
  338. }
  339. // ParseV2LoginPacket parses a V2 login packet
  340. // V2 LOGIN format (verified against licq V2 client icqpacket.cpp CPU_Logon):
  341. //
  342. // After the 10-byte header (VER+CMD+SEQ+UIN), the data payload is:
  343. //
  344. // PORT(4) + PWD_LEN(2) + PASSWORD + X1(4) + USER_IP(4) + X2(1) +
  345. // STATUS(2) + DC_VERSION(2) + X3(12)
  346. //
  347. // Note: STATUS is 2 bytes (unsigned short), NOT 4 bytes. The client's
  348. // m_nLogonStatus is declared as unsigned short in icqpacket.h.
  349. // DC_VERSION follows immediately after status (also unsigned short).
  350. func ParseV2LoginPacket(data []byte) (*LegacyLoginPacket, error) {
  351. if len(data) < 15 { // Minimum: PORT(4) + PWD_LEN(2) + 1 char + null + X1(4) + IP(4)
  352. return nil, fmt.Errorf("login packet too short: %d bytes", len(data))
  353. }
  354. r := bytes.NewReader(data)
  355. pkt := &LegacyLoginPacket{}
  356. // Read TCP port (4 bytes, little-endian) - m_nLocalPort is unsigned long
  357. var port uint32
  358. if err := binary.Read(r, binary.LittleEndian, &port); err != nil {
  359. return nil, fmt.Errorf("parsing port: %w", err)
  360. }
  361. pkt.Port = uint16(port)
  362. // Read password (length-prefixed string)
  363. password, err := ParseLegacyString(r, true)
  364. if err != nil {
  365. return nil, fmt.Errorf("parsing password: %w", err)
  366. }
  367. pkt.Password = password
  368. // Read X1 (4 bytes, usually { 0x72, 0x00, 0x04, 0x00 })
  369. var x1 uint32
  370. if err := binary.Read(r, binary.LittleEndian, &x1); err != nil {
  371. return nil, fmt.Errorf("parsing X1: %w", err)
  372. }
  373. // Read IP (4 bytes) - m_nLocalIP, stored in packet byte order
  374. ip := make([]byte, 4)
  375. if _, err := io.ReadFull(r, ip); err != nil {
  376. return nil, fmt.Errorf("parsing IP: %w", err)
  377. }
  378. pkt.IP = net.IP(ip)
  379. // Read X2 (1 byte, usually 0x04) - m_aUnknown_2
  380. var x2 uint8
  381. if err := binary.Read(r, binary.LittleEndian, &x2); err != nil {
  382. return nil, fmt.Errorf("parsing X2: %w", err)
  383. }
  384. // Read status (2 bytes) - m_nLogonStatus is unsigned short
  385. var status uint16
  386. if err := binary.Read(r, binary.LittleEndian, &status); err != nil {
  387. return nil, fmt.Errorf("parsing status: %w", err)
  388. }
  389. pkt.Status = uint32(status)
  390. // Read DC version (2 bytes) - m_nTcpVersion is unsigned short
  391. // TODO: store dcVersion in LegacyLoginPacket so V2 handler can pass it
  392. // to SetDirectConnectionInfo when direct connections are enabled for V2.
  393. var dcVersion uint16
  394. if err := binary.Read(r, binary.LittleEndian, &dcVersion); err != nil {
  395. // Optional field
  396. return pkt, nil
  397. }
  398. // Read X3 (12 bytes) - m_aUnknown_3
  399. // Byte 8-9 of X3 contains the internal connection ID used for routing
  400. // the login reply back to the correct wizard page (from sub_46C0B6 disasm).
  401. x3 := make([]byte, 12)
  402. if _, err := io.ReadFull(r, x3); err != nil {
  403. // Optional trailing data
  404. return pkt, nil
  405. }
  406. if len(x3) >= 10 {
  407. pkt.ConnectionID = binary.LittleEndian.Uint16(x3[8:10])
  408. }
  409. return pkt, nil
  410. }
  411. // BuildV2LoginReply creates a login success response
  412. // V2 LOGIN_REPLY format (from protocol spec):
  413. // USER_UIN(4) + USER_IP(4) + LOGIN_SEQ_NUM(2) + X1(4) + X2(4) + X3(4) + X4(4) + X5(7) = 33 bytes
  414. // Format from real ICQ server capture (licq source):
  415. // 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
  416. func BuildV2LoginReply(serverSeqNum uint16, clientConnectionID uint16, uin uint32, clientIP net.IP) *V2ServerPacket {
  417. data := make([]byte, 33)
  418. offset := 0
  419. // USER_UIN (4 bytes)
  420. binary.LittleEndian.PutUint32(data[offset:], uin)
  421. offset += 4
  422. // USER_IP (4 bytes) - server's view of client IP
  423. if clientIP != nil {
  424. ip4 := clientIP.To4()
  425. if ip4 != nil {
  426. copy(data[offset:offset+4], ip4)
  427. }
  428. }
  429. offset += 4
  430. // LOGIN_SEQ_NUM (2 bytes)
  431. binary.LittleEndian.PutUint16(data[offset:], clientConnectionID)
  432. offset += 2
  433. // X1 (4 bytes): 01 00 05 00
  434. data[offset] = 0x01
  435. data[offset+1] = 0x00
  436. data[offset+2] = 0x05
  437. data[offset+3] = 0x00
  438. offset += 4
  439. // X2 (4 bytes): 00 00 00 00
  440. offset += 4
  441. // X3 (4 bytes): 8C 00 00 00
  442. data[offset] = 0x8C
  443. offset += 4
  444. // X4 (4 bytes): F0 00 0A 00
  445. data[offset] = 0xF0
  446. data[offset+1] = 0x00
  447. data[offset+2] = 0x0A
  448. data[offset+3] = 0x00
  449. offset += 4
  450. // X5 (7 bytes): 0A 00 05 00 0A 00 01
  451. data[offset] = 0x0A
  452. data[offset+1] = 0x00
  453. data[offset+2] = 0x05
  454. data[offset+3] = 0x00
  455. data[offset+4] = 0x0A
  456. data[offset+5] = 0x00
  457. data[offset+6] = 0x01
  458. return &V2ServerPacket{
  459. Version: ICQLegacyVersionV2,
  460. Command: ICQLegacySrvHello,
  461. SeqNum: serverSeqNum,
  462. Data: data,
  463. }
  464. }
  465. // BuildV2BadPassword creates a bad password response
  466. func BuildV2BadPassword(seqNum uint16) *V2ServerPacket {
  467. return &V2ServerPacket{
  468. Version: ICQLegacyVersionV2,
  469. Command: ICQLegacySrvWrongPasswd,
  470. SeqNum: seqNum,
  471. }
  472. }
  473. // BuildV2UserOnline creates a user online notification
  474. // V2 USER_ONLINE format (verified against licq V2 client icq-udp.h):
  475. //
  476. // The client reads:
  477. //
  478. // UIN(4) + IP(4) + PORT(2) + JUNK(4+2+1) + STATUS(2) + TRAILING(4+2)
  479. //
  480. // Total = 25 bytes
  481. //
  482. // Field breakdown matching the client's read order:
  483. //
  484. // REMOTE_UIN(4) + REMOTE_IP(4) + REMOTE_PORT(2) + REMOTE_REAL_IP(4) +
  485. // DC_VERSION(2) + X1(1) + STATUS(2) + X2(4) + X3(2) = 25 bytes
  486. //
  487. // The client treats bytes 10-16 as "junk" (it reads them into junkLong,
  488. // junkShort, junkChar but discards the values). STATUS must be at offset 17
  489. // as a uint16 LE. PORT must be at offset 8 as a uint16 LE.
  490. func BuildV2UserOnline(seqNum uint16, uin uint32, status uint32, ip net.IP, port uint16) *V2ServerPacket {
  491. data := make([]byte, 25)
  492. offset := 0
  493. // REMOTE_UIN (4 bytes)
  494. binary.LittleEndian.PutUint32(data[offset:], uin)
  495. offset += 4
  496. // REMOTE_IP (4 bytes) - "outer" IP address
  497. if ip != nil {
  498. ip4 := ip.To4()
  499. if ip4 != nil {
  500. copy(data[offset:offset+4], ip4)
  501. }
  502. }
  503. offset += 4
  504. // REMOTE_PORT (2 bytes, little-endian) - TCP port for direct connection
  505. // Client reads this as unsigned short (2 bytes)
  506. binary.LittleEndian.PutUint16(data[offset:], port)
  507. offset += 2
  508. // Next 7 bytes are read by client as junkLong(4) + junkShort(2) + junkChar(1)
  509. // but discarded. We fill them with meaningful data matching the original
  510. // Mirabilis server format: REAL_IP(4) + DC_VERSION(2) + X1(1)
  511. // REMOTE_REAL_IP (4 bytes) - "inner" IP address
  512. if ip != nil {
  513. ip4 := ip.To4()
  514. if ip4 != nil {
  515. copy(data[offset:offset+4], ip4)
  516. }
  517. }
  518. offset += 4
  519. // DC_VERSION (2 bytes) - client's DC version (junkShort)
  520. binary.LittleEndian.PutUint16(data[offset:], 0x0003)
  521. offset += 2
  522. // X1 (1 byte): 0x04 (junkChar)
  523. data[offset] = 0x04
  524. offset++
  525. // STATUS (2 bytes, little-endian) - client reads as unsigned short
  526. binary.LittleEndian.PutUint16(data[offset:], uint16(status))
  527. offset += 2
  528. // X2 (4 bytes): trailing data
  529. data[offset] = 0x02
  530. data[offset+1] = 0x00
  531. data[offset+2] = 0x00
  532. data[offset+3] = 0x00
  533. offset += 4
  534. // X3 (2 bytes): more trailing data (total = 25 bytes)
  535. data[offset] = 0x00
  536. data[offset+1] = 0x00
  537. return &V2ServerPacket{
  538. Version: ICQLegacyVersionV2,
  539. Command: ICQLegacySrvUserOnline,
  540. SeqNum: seqNum,
  541. Data: data,
  542. }
  543. }
  544. // BuildV2UserOffline creates a user offline notification
  545. func BuildV2UserOffline(seqNum uint16, uin uint32) *V2ServerPacket {
  546. data := make([]byte, 4)
  547. binary.LittleEndian.PutUint32(data[0:4], uin)
  548. return &V2ServerPacket{
  549. Version: ICQLegacyVersionV2,
  550. Command: ICQLegacySrvUserOffline,
  551. SeqNum: seqNum,
  552. Data: data,
  553. }
  554. }
  555. // BuildV2Ack creates an acknowledgment packet
  556. func BuildV2Ack(seqNum uint16) *V2ServerPacket {
  557. return &V2ServerPacket{
  558. Version: ICQLegacyVersionV2,
  559. Command: ICQLegacySrvAck,
  560. SeqNum: seqNum,
  561. }
  562. }
  563. // BuildV2OfflineMessage creates an offline message delivery packet (SRV_SYS_MSG_OFFLINE 0x00DC).
  564. // The client (licq icq-udp.h case ICQ_CMDxRCV_SYSxMSGxOFFLINE) expects:
  565. //
  566. // UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MIN(1) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  567. //
  568. // This preserves the original message timestamp, unlike SRV_SYS_MSG_ONLINE (0x0104)
  569. // which the client treats as "received now".
  570. func BuildV2OfflineMessage(seqNum uint16, fromUIN uint32, msgType uint16, message string, timestamp time.Time) *V2ServerPacket {
  571. buf := new(bytes.Buffer)
  572. _ = binary.Write(buf, binary.LittleEndian, fromUIN)
  573. _ = binary.Write(buf, binary.LittleEndian, uint16(timestamp.Year()))
  574. buf.WriteByte(byte(timestamp.Month()))
  575. buf.WriteByte(byte(timestamp.Day()))
  576. buf.WriteByte(byte(timestamp.Hour()))
  577. buf.WriteByte(byte(timestamp.Minute()))
  578. _ = binary.Write(buf, binary.LittleEndian, msgType)
  579. _ = WriteLegacyString(buf, message)
  580. return &V2ServerPacket{
  581. Version: ICQLegacyVersionV2,
  582. Command: ICQLegacySrvSysMsgOffline,
  583. SeqNum: seqNum,
  584. Data: buf.Bytes(),
  585. }
  586. }
  587. // BuildV2Message creates a message delivery packet
  588. func BuildV2Message(seqNum uint16, fromUIN uint32, msgType uint16, message string) *V2ServerPacket {
  589. buf := new(bytes.Buffer)
  590. _ = binary.Write(buf, binary.LittleEndian, fromUIN)
  591. _ = binary.Write(buf, binary.LittleEndian, msgType)
  592. _ = WriteLegacyString(buf, message)
  593. return &V2ServerPacket{
  594. Version: ICQLegacyVersionV2,
  595. Command: ICQLegacySrvSysMsgOnline,
  596. SeqNum: seqNum,
  597. Data: buf.Bytes(),
  598. }
  599. }
  600. // BuildV2StatusUpdate creates a status update notification
  601. func BuildV2StatusUpdate(seqNum uint16, uin uint32, status uint32) *V2ServerPacket {
  602. data := make([]byte, 8)
  603. binary.LittleEndian.PutUint32(data[0:4], uin)
  604. binary.LittleEndian.PutUint32(data[4:8], status)
  605. return &V2ServerPacket{
  606. Version: ICQLegacyVersionV2,
  607. Command: ICQLegacySrvUserStatus,
  608. SeqNum: seqNum,
  609. Data: data,
  610. }
  611. }
  612. // BuildV2ContactListDone creates a contact list processed response
  613. // V2 format: UIN(4) — from licq example: 02 00 1C 02 05 00 8F 76 20 00
  614. func BuildV2ContactListDone(seqNum uint16, uin uint32) *V2ServerPacket {
  615. data := make([]byte, 4)
  616. binary.LittleEndian.PutUint32(data[0:4], uin)
  617. return &V2ServerPacket{
  618. Version: ICQLegacyVersionV2,
  619. Command: ICQLegacySrvUserListDone,
  620. SeqNum: seqNum,
  621. Data: data,
  622. }
  623. }
  624. // BuildV2SearchResult creates a search result packet
  625. // V2 format (from center-1.10.7 icq_HandleSearchReply / icq_HandleInfoReply):
  626. // V2 search found format (from licq):
  627. // SEQ(2) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
  628. // V2 search done format (from licq): SEQ(2) + MORE(1)
  629. func BuildV2SearchResult(seqNum uint16, user *LegacyUserInfo, isLast bool) *V2ServerPacket {
  630. buf := new(bytes.Buffer)
  631. _ = binary.Write(buf, binary.LittleEndian, seqNum)
  632. if user.UIN == 0 && isLast {
  633. // No results — just send search done with more=0
  634. buf.WriteByte(0x00)
  635. return &V2ServerPacket{
  636. Version: ICQLegacyVersionV2,
  637. Command: ICQLegacySrvSearchDone,
  638. SeqNum: seqNum,
  639. Data: buf.Bytes(),
  640. }
  641. }
  642. // Send search found with user data
  643. _ = binary.Write(buf, binary.LittleEndian, user.UIN)
  644. _ = WriteLegacyString(buf, user.Nickname)
  645. _ = WriteLegacyString(buf, user.FirstName)
  646. _ = WriteLegacyString(buf, user.LastName)
  647. _ = WriteLegacyString(buf, user.Email)
  648. buf.WriteByte(user.Auth)
  649. return &V2ServerPacket{
  650. Version: ICQLegacyVersionV2,
  651. Command: ICQLegacySrvSearchFound,
  652. SeqNum: seqNum,
  653. Data: buf.Bytes(),
  654. }
  655. }
  656. // BuildV2InfoReply creates a basic info reply packet (SRV_INFO_REPLY 0x0118)
  657. // V2 format (from center-1.10.7 icq_HandleInfoReply):
  658. // SEQ(2) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
  659. // Same payload format as search result, but uses command 0x0118
  660. func BuildV2InfoReply(serverSeq uint16, checkSeq uint16, user *LegacyUserInfo) *V2ServerPacket {
  661. buf := new(bytes.Buffer)
  662. // checkSequence - echoes the client's info sub-sequence so DoneExtendedEvent can match
  663. _ = binary.Write(buf, binary.LittleEndian, checkSeq)
  664. _ = binary.Write(buf, binary.LittleEndian, user.UIN)
  665. _ = WriteLegacyString(buf, user.Nickname)
  666. _ = WriteLegacyString(buf, user.FirstName)
  667. _ = WriteLegacyString(buf, user.LastName)
  668. _ = WriteLegacyString(buf, user.Email)
  669. // AUTH byte (0 = auth not required, 1 = auth required)
  670. buf.WriteByte(user.Auth)
  671. // Trailing padding (observed in licq example packets)
  672. _ = binary.Write(buf, binary.LittleEndian, uint16(0))
  673. _ = binary.Write(buf, binary.LittleEndian, uint16(0))
  674. return &V2ServerPacket{
  675. Version: ICQLegacyVersionV2,
  676. Command: ICQLegacySrvInfoReply,
  677. SeqNum: serverSeq,
  678. Data: buf.Bytes(),
  679. }
  680. }
  681. // BuildV2ExtInfoReply creates an extended info reply packet (SRV_EXT_INFO_REPLY 0x0122)
  682. // V2 format (from center-1.10.7 icq_HandleExtInfoReply):
  683. // SEQ(2) + UIN(4) + CITY_LEN(2) + CITY + COUNTRY(2) + COUNTRY_STAT(1) + STATE_LEN(2) + STATE +
  684. // AGE(2) + GENDER(1) + PHONE_LEN(2) + PHONE + HP_LEN(2) + HP + ABOUT_LEN(2) + ABOUT
  685. func BuildV2ExtInfoReply(serverSeq uint16, checkSeq uint16, user *LegacyUserInfo) *V2ServerPacket {
  686. buf := new(bytes.Buffer)
  687. // checkSequence - echoes the client's info sub-sequence
  688. _ = binary.Write(buf, binary.LittleEndian, checkSeq)
  689. // UIN
  690. _ = binary.Write(buf, binary.LittleEndian, user.UIN)
  691. // City (length-prefixed string)
  692. _ = WriteLegacyString(buf, user.City)
  693. // Country code (2 bytes)
  694. _ = binary.Write(buf, binary.LittleEndian, user.Country)
  695. // Country stat (1 byte) - 0 = not specified
  696. buf.WriteByte(0)
  697. // State (length-prefixed string)
  698. _ = WriteLegacyString(buf, user.State)
  699. // Age (2 bytes)
  700. _ = binary.Write(buf, binary.LittleEndian, user.Age)
  701. // Gender (1 byte)
  702. buf.WriteByte(user.Gender)
  703. // Phone (length-prefixed string)
  704. _ = WriteLegacyString(buf, user.Phone)
  705. // Homepage (length-prefixed string)
  706. _ = WriteLegacyString(buf, user.Homepage)
  707. // About (length-prefixed string)
  708. _ = WriteLegacyString(buf, user.About)
  709. return &V2ServerPacket{
  710. Version: ICQLegacyVersionV2,
  711. Command: ICQLegacySrvExtInfoReply,
  712. SeqNum: serverSeq,
  713. Data: buf.Bytes(),
  714. }
  715. }
  716. // BuildV2NewUIN creates a SRV_NEW_UIN (0x0046) response for registration.
  717. // The client (center icq_InitNewUser) reads the new UIN from pak.data[2]:
  718. //
  719. // icq_Uin = Chars_2_DW(&pak.data[2])
  720. //
  721. // So the data format is: SEQ(2) + UIN(4)
  722. // The server packet uses the srv_net_icq_pak 6-byte header (ver+cmd+seq).
  723. func BuildV2NewUIN(seqNum uint16, newUIN uint32) *V2ServerPacket {
  724. data := make([]byte, 6)
  725. binary.LittleEndian.PutUint16(data[0:2], seqNum)
  726. binary.LittleEndian.PutUint32(data[2:6], newUIN)
  727. return &V2ServerPacket{
  728. Version: ICQLegacyVersionV2,
  729. Command: ICQLegacySrvNewUIN,
  730. SeqNum: seqNum,
  731. Data: data,
  732. }
  733. }
  734. // ParseV2ContactList parses a contact list packet
  735. func ParseV2ContactList(data []byte) (*LegacyContactListPacket, error) {
  736. if len(data) < 1 {
  737. return nil, fmt.Errorf("contact list packet too short")
  738. }
  739. r := bytes.NewReader(data)
  740. var count uint8
  741. if err := binary.Read(r, binary.LittleEndian, &count); err != nil {
  742. return nil, err
  743. }
  744. pkt := &LegacyContactListPacket{
  745. UINs: make([]uint32, 0, count),
  746. }
  747. for i := uint8(0); i < count; i++ {
  748. var uin uint32
  749. if err := binary.Read(r, binary.LittleEndian, &uin); err != nil {
  750. break // May have fewer UINs than count
  751. }
  752. pkt.UINs = append(pkt.UINs, uin)
  753. }
  754. return pkt, nil
  755. }
  756. // ParseV2Message parses a message packet
  757. func ParseV2Message(data []byte) (*LegacyMessagePacket, error) {
  758. if len(data) < 6 {
  759. return nil, fmt.Errorf("message packet too short")
  760. }
  761. r := bytes.NewReader(data)
  762. pkt := &LegacyMessagePacket{}
  763. if err := binary.Read(r, binary.LittleEndian, &pkt.ToUIN); err != nil {
  764. return nil, err
  765. }
  766. if err := binary.Read(r, binary.LittleEndian, &pkt.MsgType); err != nil {
  767. return nil, err
  768. }
  769. msg, err := ParseLegacyString(r, true)
  770. if err != nil {
  771. return nil, err
  772. }
  773. pkt.Message = msg
  774. // For URL messages, parse description
  775. if pkt.MsgType == ICQLegacyMsgURL {
  776. // URL format: "description\xFEurl"
  777. parts := bytes.SplitN([]byte(pkt.Message), []byte{0xFE}, 2)
  778. if len(parts) == 2 {
  779. pkt.Desc = string(parts[0])
  780. pkt.URL = string(parts[1])
  781. } else {
  782. pkt.URL = pkt.Message
  783. }
  784. }
  785. return pkt, nil
  786. }