v2_handler.go 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  1. package icq_legacy
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/binary"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "time"
  10. "github.com/mk6i/open-oscar-server/state"
  11. )
  12. // V2Handler handles ICQ V2 protocol packets
  13. type V2Handler struct {
  14. BaseHandler
  15. packetBuilder V2PacketBuilder
  16. dispatcher MessageDispatcher
  17. }
  18. // NewV2Handler creates a new V2 protocol handler
  19. func NewV2Handler(
  20. sessions *LegacySessionManager,
  21. service LegacyService,
  22. sender PacketSender,
  23. packetBuilder V2PacketBuilder,
  24. logger *slog.Logger,
  25. ) *V2Handler {
  26. return &V2Handler{
  27. BaseHandler: BaseHandler{
  28. sessions: sessions,
  29. service: service,
  30. sender: sender,
  31. logger: logger,
  32. },
  33. packetBuilder: packetBuilder,
  34. }
  35. }
  36. // SetSender sets the packet sender (for circular dependency resolution)
  37. func (h *V2Handler) SetSender(sender PacketSender) {
  38. h.sender = sender
  39. }
  40. // SetDispatcher sets the message dispatcher for cross-protocol message routing
  41. func (h *V2Handler) SetDispatcher(dispatcher MessageDispatcher) {
  42. h.dispatcher = dispatcher
  43. }
  44. // Handle processes a V2 protocol packet
  45. func (h *V2Handler) Handle(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
  46. // Debug: dump raw packet
  47. h.logger.Debug("raw V2 packet",
  48. "hex", fmt.Sprintf("%X", packet),
  49. "len", len(packet),
  50. )
  51. pkt, err := UnmarshalV2ClientPacket(packet)
  52. if err != nil {
  53. return fmt.Errorf("parsing V2 packet: %w", err)
  54. }
  55. h.logger.Debug("V2 packet received",
  56. "command", fmt.Sprintf("0x%04X", pkt.Command),
  57. "uin", pkt.UIN,
  58. "seq", pkt.SeqNum,
  59. "addr", addr.String(),
  60. "data_hex", fmt.Sprintf("%X", pkt.Data),
  61. )
  62. // Update session activity if we have one
  63. if session != nil {
  64. session.UpdateActivity()
  65. session.SeqNumClient = pkt.SeqNum
  66. }
  67. // If no session exists and this is not a login/registration/ack command,
  68. // send NOT_CONNECTED to force the client to reconnect.
  69. if session == nil {
  70. switch pkt.Command {
  71. case ICQLegacyCmdAck, ICQLegacyCmdLogin,
  72. ICQLegacyCmdFirstLogin, ICQLegacyCmdGetDeps,
  73. ICQLegacyCmdRegNewUser, ICQLegacyCmdExtInfoReq:
  74. // Allow these through - they don't require a session
  75. default:
  76. h.logger.Info("V2 packet from unknown session, sending NOT_CONNECTED",
  77. "command", fmt.Sprintf("0x%04X", pkt.Command),
  78. "uin", pkt.UIN,
  79. "addr", addr.String(),
  80. )
  81. return h.sendNotConnectedToAddr(addr, pkt.SeqNum)
  82. }
  83. }
  84. switch pkt.Command {
  85. case ICQLegacyCmdAck:
  86. return h.handleAck(session, pkt)
  87. case ICQLegacyCmdLogin:
  88. return h.handleLogin(session, addr, pkt)
  89. case ICQLegacyCmdLogoff:
  90. return h.handleLogoff(session, pkt)
  91. case ICQLegacyCmdKeepAlive, ICQLegacyCmdKeepAlive2:
  92. return h.handleKeepAlive(session, pkt)
  93. case ICQLegacyCmdContactList:
  94. return h.handleContactList(session, pkt)
  95. case ICQLegacyCmdThruServer:
  96. return h.handleSendMessage(session, pkt)
  97. case ICQLegacyCmdAuthorize:
  98. return h.handleAuthorize(session, pkt)
  99. case ICQLegacyCmdSetStatus:
  100. return h.handleSetStatus(session, pkt)
  101. case ICQLegacyCmdInfoReq:
  102. return h.handleInfoReq(session, pkt)
  103. case ICQLegacyCmdExtInfoReq:
  104. return h.handleExtInfoReq(session, addr, pkt)
  105. case ICQLegacyCmdSearchUIN:
  106. return h.handleSearchUIN(session, pkt)
  107. case ICQLegacyCmdSearchUser:
  108. return h.handleSearchUser(session, pkt)
  109. case ICQLegacyCmdSysMsgReq:
  110. return h.handleOfflineMsgReq(session, pkt)
  111. case ICQLegacyCmdSysMsgDoneAck:
  112. return h.handleOfflineMsgAck(session, pkt)
  113. case ICQLegacyCmdVisibleList:
  114. return h.handleVisibleList(session, pkt)
  115. case ICQLegacyCmdInvisibleList:
  116. return h.handleInvisibleList(session, pkt)
  117. case ICQLegacyCmdUserAdd:
  118. return h.handleUserAdd(session, pkt)
  119. case ICQLegacyCmdUpdateBasic, ICQLegacyCmdSetBasicInfo:
  120. return h.handleUpdateBasic(session, pkt)
  121. case ICQLegacyCmdUpdateDetail:
  122. return h.handleUpdateDetail(session, pkt)
  123. case ICQLegacyCmdGetDeps:
  124. // 0x03F2 - Pre-auth pseudo-login (historically "get departments list" in iserverd).
  125. // Some clients send version=2 in the header but use V3 packet structure.
  126. // We re-parse the raw packet as V3 format (12-byte header) inline.
  127. return h.handleGetDeps(addr, packet)
  128. case ICQLegacyCmdFirstLogin:
  129. return h.handleFirstLogin(session, addr, pkt)
  130. case ICQLegacyCmdRegNewUser:
  131. return h.handleRegNewUser(addr, pkt)
  132. default:
  133. h.logger.Debug("unhandled V2 command",
  134. "command", fmt.Sprintf("0x%04X", pkt.Command),
  135. "uin", pkt.UIN,
  136. )
  137. // Send ACK for unknown commands to keep client happy
  138. if session != nil {
  139. return h.sendAck(session, pkt.SeqNum)
  140. }
  141. return nil
  142. }
  143. }
  144. // handleAck processes an acknowledgment packet
  145. func (h *V2Handler) handleAck(session *LegacySession, pkt *V2ClientPacket) error {
  146. // ACKs don't require a response
  147. h.logger.Debug("received ACK", "seq", pkt.SeqNum)
  148. return nil
  149. }
  150. // handleLogin processes a login request
  151. // Refactored to use service layer (AuthenticateUser) and packet builder.
  152. // Following the OSCAR pattern: unmarshal -> call service -> build response
  153. func (h *V2Handler) handleLogin(session *LegacySession, addr *net.UDPAddr, pkt *V2ClientPacket) error {
  154. ctx := context.Background()
  155. // 1. Unmarshal packet to typed struct
  156. loginData, err := ParseV2LoginPacket(pkt.Data)
  157. if err != nil {
  158. h.logger.Debug("failed to parse login packet", "err", err)
  159. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(pkt.SeqNum, pkt.Version))
  160. }
  161. loginData.UIN = pkt.UIN
  162. // Extract LOGIN_SEQ_NUM from payload.
  163. // From protocol doc and center-1.10.7 source: after PORT(4) + PASSWORD(2+len),
  164. // the login_2 struct has: X1(4) + IP(4) + X2(1) + STATUS(4) + X3(4) + SEQ(2)
  165. // = 17 bytes to reach SEQ. Total offset = 4 + 2 + pwdLen + 17
  166. var loginSeqNum uint16
  167. if len(pkt.Data) >= 6 {
  168. pwdLen := binary.LittleEndian.Uint16(pkt.Data[4:6])
  169. seqOffset := 4 + 2 + int(pwdLen) + 17
  170. if seqOffset+2 <= len(pkt.Data) {
  171. loginSeqNum = binary.LittleEndian.Uint16(pkt.Data[seqOffset : seqOffset+2])
  172. }
  173. }
  174. h.logger.Info("login attempt",
  175. "uin", pkt.UIN,
  176. "ip", loginData.IP,
  177. "port", loginData.Port,
  178. "status", loginData.Status,
  179. )
  180. // 2. Call service layer with typed request
  181. authReq := AuthRequest{
  182. UIN: pkt.UIN,
  183. Password: loginData.Password,
  184. Status: loginData.Status,
  185. TCPPort: uint32(loginData.Port),
  186. Version: pkt.Version,
  187. }
  188. authResult, err := h.service.AuthenticateUser(ctx, authReq)
  189. if err != nil {
  190. h.logger.Error("authentication error", "err", err, "uin", pkt.UIN)
  191. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(pkt.SeqNum, pkt.Version))
  192. }
  193. if !authResult.Success {
  194. h.logger.Info("login failed - invalid credentials", "uin", pkt.UIN, "error_code", authResult.ErrorCode)
  195. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(pkt.SeqNum, pkt.Version))
  196. }
  197. // 3. Create session
  198. newSession, err := h.sessions.CreateSession(pkt.UIN, addr, pkt.Version, authResult.oscarSession)
  199. if err != nil {
  200. h.logger.Error("failed to create session", "err", err, "uin", pkt.UIN)
  201. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(pkt.SeqNum, pkt.Version))
  202. }
  203. newSession.SetStatus(loginData.Status)
  204. newSession.Password = loginData.Password
  205. // 4. Send ACK first, then login reply
  206. ackPkt := h.packetBuilder.BuildAck(pkt.SeqNum, pkt.Version)
  207. if err := h.sender.SendToSession(newSession, ackPkt); err != nil {
  208. return err
  209. }
  210. // 5. Build and send login reply — echo back LOGIN_SEQ_NUM from payload
  211. loginReplyPkt := h.packetBuilder.BuildLoginReply(newSession, loginSeqNum)
  212. if err := h.sender.SendToSession(newSession, loginReplyPkt); err != nil {
  213. return err
  214. }
  215. h.logger.Info("login successful",
  216. "uin", pkt.UIN,
  217. "session_id", newSession.SessionID,
  218. )
  219. if err := h.service.NotifyUserOnline(ctx, newSession.UIN, newSession.GetStatus()); err != nil {
  220. h.logger.Debug("V2 failed to notify OSCAR clients of online", "uin", newSession.UIN, "err", err)
  221. }
  222. return nil
  223. }
  224. // handleLogoff processes a logout request
  225. func (h *V2Handler) handleLogoff(session *LegacySession, pkt *V2ClientPacket) error {
  226. if session == nil {
  227. return nil
  228. }
  229. h.logger.Info("logout", "uin", session.UIN)
  230. // Notify contacts that user is offline
  231. h.sessions.BroadcastToContacts(session, func(contact *LegacySession) {
  232. _ = h.sendUserOffline(contact, session.UIN)
  233. })
  234. // Notify OSCAR clients that this user went offline
  235. ctx := context.Background()
  236. if err := h.service.NotifyUserOffline(ctx, session.UIN); err != nil {
  237. h.logger.Debug("V2 failed to notify OSCAR clients of offline", "uin", session.UIN, "err", err)
  238. }
  239. // Remove session
  240. h.sessions.RemoveSession(session.UIN)
  241. return nil
  242. }
  243. // sendNotConnectedToAddr sends a NOT_CONNECTED (0x00F0) response to a V2 client
  244. // address that has no session. This forces the client to reconnect.
  245. // Uses V2 server packet format: VERSION(2) + COMMAND(2) + SEQ(2)
  246. func (h *V2Handler) sendNotConnectedToAddr(addr *net.UDPAddr, seqNum uint16) error {
  247. pkt := MarshalV2ServerPacket(&V2ServerPacket{
  248. Version: ICQLegacyVersionV2,
  249. Command: ICQLegacySrvNotConnected,
  250. SeqNum: seqNum,
  251. })
  252. return h.sender.SendPacket(addr, pkt)
  253. }
  254. // handleKeepAlive processes a keep-alive ping
  255. func (h *V2Handler) handleKeepAlive(session *LegacySession, pkt *V2ClientPacket) error {
  256. if session == nil {
  257. return nil // Already handled by the session-nil guard above
  258. }
  259. // Session activity already updated, just send ACK
  260. return h.sendAck(session, pkt.SeqNum)
  261. }
  262. // handleContactList processes a contact list update
  263. // Refactored to use service layer (ProcessContactList) and packet builder.
  264. // Following the OSCAR pattern: unmarshal -> call service -> build response
  265. //
  266. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  267. // receives SRV_ACK with the matching sequence number. We must send ACK first,
  268. // then the data responses. Without the ACK, the client retransmits in a loop.
  269. func (h *V2Handler) handleContactList(session *LegacySession, pkt *V2ClientPacket) error {
  270. if session == nil {
  271. return nil
  272. }
  273. ctx := context.Background()
  274. // Send ACK first to stop client retransmission
  275. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  276. return err
  277. }
  278. // 1. Unmarshal packet to typed struct
  279. contactList, err := ParseV2ContactList(pkt.Data)
  280. if err != nil {
  281. h.logger.Debug("failed to parse contact list", "err", err)
  282. return nil
  283. }
  284. h.logger.Debug("contact list received",
  285. "uin", session.UIN,
  286. "contacts", len(contactList.UINs),
  287. )
  288. // Update session contact list (handler responsibility - session management)
  289. session.SetContactList(contactList.UINs)
  290. // 2. Call service layer with typed request
  291. contactReq := ContactListRequest{
  292. UIN: session.UIN,
  293. Contacts: contactList.UINs,
  294. }
  295. contactResult, err := h.service.ProcessContactList(ctx, session.Instance, contactReq)
  296. if err != nil {
  297. h.logger.Debug("failed to process contact list", "err", err)
  298. // Still send contact list done even on error
  299. return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), session.UIN))
  300. }
  301. // 3. Build and send responses using packet builder
  302. // Send online status for each contact that is online.
  303. // Apply downgradeStatusForV2 so V2 clients see correct icons
  304. // for statuses they don't natively support (N/A→Away, Occupied→DND, FFC→Online).
  305. for _, contact := range contactResult.OnlineContacts {
  306. if contact.Online {
  307. onlinePkt := h.packetBuilder.BuildUserOnline(
  308. session.NextServerSeqNum(),
  309. contact.UIN,
  310. downgradeStatusForV2(contact.Status),
  311. nil, // IP not available from service layer
  312. 0, // Port not available from service layer
  313. )
  314. _ = h.sender.SendToSession(session, onlinePkt)
  315. }
  316. }
  317. // 4. Send contact list done using packet builder
  318. return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), session.UIN))
  319. }
  320. // handleSendMessage processes a message send request
  321. // Refactored to use service layer (ProcessMessage) and packet builder.
  322. // Following the OSCAR pattern: unmarshal -> call service -> build response
  323. //
  324. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  325. // receives SRV_ACK with the matching sequence number. We must send ACK
  326. // to stop the retransmission loop.
  327. func (h *V2Handler) handleSendMessage(session *LegacySession, pkt *V2ClientPacket) error {
  328. if session == nil {
  329. return nil
  330. }
  331. ctx := context.Background()
  332. // Send ACK first to stop client retransmission
  333. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  334. return err
  335. }
  336. // 1. Unmarshal packet to typed struct
  337. msg, err := ParseV2Message(pkt.Data)
  338. if err != nil {
  339. h.logger.Debug("failed to parse message", "err", err)
  340. return nil
  341. }
  342. msg.FromUIN = session.UIN
  343. h.logger.Debug("message received",
  344. "from", msg.FromUIN,
  345. "to", msg.ToUIN,
  346. "type", msg.MsgType,
  347. )
  348. // 2. Call service layer with typed request
  349. msgReq := MessageRequest{
  350. FromUIN: msg.FromUIN,
  351. ToUIN: msg.ToUIN,
  352. MsgType: msg.MsgType,
  353. Message: msg.Message,
  354. }
  355. msgResult, err := h.service.ProcessMessage(ctx, session, msgReq)
  356. if err != nil {
  357. h.logger.Debug("failed to process message", "err", err)
  358. } else {
  359. h.logger.Debug("message processed",
  360. "from", msg.FromUIN,
  361. "to", msg.ToUIN,
  362. "delivered", msgResult.Delivered,
  363. "stored_offline", msgResult.StoredOffline,
  364. "target_online", msgResult.TargetOnline,
  365. )
  366. // Deliver message to target if online via legacy protocol
  367. if msgResult.TargetOnline {
  368. targetSession := h.sessions.GetSession(msg.ToUIN)
  369. if targetSession != nil {
  370. if h.dispatcher != nil {
  371. _ = h.dispatcher.SendOnlineMessage(targetSession, msg.FromUIN, msg.MsgType, msg.Message)
  372. } else {
  373. _ = h.sendMessage(targetSession, msg.FromUIN, msg.MsgType, msg.Message)
  374. }
  375. }
  376. }
  377. }
  378. return nil
  379. }
  380. // handleSetStatus processes a status change request
  381. // Refactored to use service layer (ProcessStatusChange) and packet builder.
  382. // Following the OSCAR pattern: unmarshal -> call service -> build response
  383. //
  384. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  385. // receives SRV_ACK with the matching sequence number. We must send ACK first.
  386. func (h *V2Handler) handleSetStatus(session *LegacySession, pkt *V2ClientPacket) error {
  387. if session == nil {
  388. return nil
  389. }
  390. ctx := context.Background()
  391. // Send ACK first to stop client retransmission
  392. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  393. return err
  394. }
  395. // 1. Unmarshal packet to typed struct
  396. if len(pkt.Data) < 4 {
  397. return nil
  398. }
  399. newStatus := binary.LittleEndian.Uint32(pkt.Data[0:4])
  400. oldStatus := session.GetStatus()
  401. h.logger.Debug("status change",
  402. "uin", session.UIN,
  403. "old_status", fmt.Sprintf("0x%08X", oldStatus),
  404. "new_status", fmt.Sprintf("0x%08X", newStatus),
  405. )
  406. // Update session status (handler responsibility - session management)
  407. session.SetStatus(newStatus)
  408. // 2. Call service layer with typed request
  409. statusReq := StatusChangeRequest{
  410. UIN: session.UIN,
  411. NewStatus: newStatus,
  412. OldStatus: oldStatus,
  413. }
  414. statusResult, err := h.service.ProcessStatusChange(ctx, statusReq)
  415. if err != nil {
  416. h.logger.Debug("failed to process status change", "err", err)
  417. } else {
  418. h.logger.Debug("status change processed",
  419. "uin", session.UIN,
  420. "notify_count", len(statusResult.NotifyTargets),
  421. )
  422. // 3. Send status notifications to contacts using dispatcher
  423. // The service layer returns the list of users to notify.
  424. // We must go through the dispatcher so each target gets the
  425. // correct packet format for its protocol version.
  426. for _, target := range statusResult.NotifyTargets {
  427. targetSession := h.sessions.GetSession(target.UIN)
  428. if targetSession != nil {
  429. if h.dispatcher != nil {
  430. _ = h.dispatcher.SendStatusChange(targetSession, session.UIN, newStatus)
  431. } else {
  432. statusPkt := h.packetBuilder.BuildStatusUpdate(
  433. targetSession.NextServerSeqNum(),
  434. session.UIN,
  435. newStatus,
  436. )
  437. _ = h.sender.SendToSession(targetSession, statusPkt)
  438. }
  439. }
  440. }
  441. }
  442. return nil
  443. }
  444. // handleInfoReq processes a user info request
  445. // Client sends: SEQ(2) + UIN(4) as data (from center-1.10.7 icq_SendInfoReq)
  446. // Server responds with SRV_INFO_REPLY (0x0118)
  447. //
  448. // IMPORTANT: This is an "extended event" in the client. The client's retry
  449. // thread is cancelled by SRV_ACK, which moves the event to the extended event
  450. // list. Then SRV_INFO_REPLY calls DoneExtendedEvent() to complete it.
  451. // Without the ACK, the client retransmits AND DoneExtendedEvent can't find
  452. // the event (it was never moved from running to extended).
  453. func (h *V2Handler) handleInfoReq(session *LegacySession, pkt *V2ClientPacket) error {
  454. if session == nil {
  455. return nil
  456. }
  457. ctx := context.Background()
  458. // Send ACK first - required for extended events
  459. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  460. return err
  461. }
  462. // Client sends SEQ(2) + UIN(4) = 6 bytes minimum
  463. if len(pkt.Data) < 6 {
  464. return nil
  465. }
  466. // Skip SEQ prefix (2 bytes), read UIN at offset 2
  467. infoSeq := binary.LittleEndian.Uint16(pkt.Data[0:2])
  468. targetUIN := binary.LittleEndian.Uint32(pkt.Data[2:6])
  469. h.logger.Debug("info request",
  470. "from", session.UIN,
  471. "target", targetUIN,
  472. "info_seq", infoSeq,
  473. )
  474. // Get user info
  475. info, err := h.service.GetUserInfo(ctx, targetUIN)
  476. if err != nil {
  477. h.logger.Debug("failed to get user info", "err", err)
  478. return nil
  479. }
  480. // Build and send SRV_INFO_REPLY (0x0118)
  481. // The checkSequence in the data payload must echo the client's info sub-sequence
  482. // so DoneExtendedEvent() can match the response to the pending request.
  483. wireInfo := &LegacyUserInfo{
  484. UIN: info.UIN,
  485. Nickname: truncateField(info.Nickname, 20, h.logger, "nickname", info.UIN),
  486. FirstName: truncateField(info.FirstName, 64, h.logger, "first_name", info.UIN),
  487. LastName: truncateField(info.LastName, 64, h.logger, "last_name", info.UIN),
  488. Email: truncateField(info.Email, 64, h.logger, "email", info.UIN),
  489. Auth: info.AuthRequired,
  490. }
  491. replyPkt := BuildV2InfoReply(session.NextServerSeqNum(), infoSeq, wireInfo)
  492. replyPkt.Version = session.Version
  493. return h.sender.SendToSession(session, MarshalV2ServerPacket(replyPkt))
  494. }
  495. // handleExtInfoReq processes an extended user info request
  496. // Client sends: SEQ(2) + UIN(4) as data (from center-1.10.7 icq_SendExtInfoReq)
  497. // Server responds with SRV_EXT_INFO_REPLY (0x0122)
  498. //
  499. // IMPORTANT: This is an "extended event" in the client - ACK must be sent
  500. // first to move the event from running to extended list, then the data reply
  501. // completes it via DoneExtendedEvent().
  502. func (h *V2Handler) handleExtInfoReq(session *LegacySession, addr *net.UDPAddr, pkt *V2ClientPacket) error {
  503. ctx := context.Background()
  504. // Client sends SEQ(2) + UIN(4) = 6 bytes minimum
  505. if len(pkt.Data) < 6 {
  506. return nil
  507. }
  508. // Skip SEQ prefix (2 bytes), read UIN at offset 2
  509. seqPrefix := binary.LittleEndian.Uint16(pkt.Data[0:2])
  510. targetUIN := binary.LittleEndian.Uint32(pkt.Data[2:6])
  511. h.logger.Debug("ext info request",
  512. "from", pkt.UIN,
  513. "target", targetUIN,
  514. "has_session", session != nil,
  515. )
  516. // Send ACK
  517. if session != nil {
  518. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  519. return err
  520. }
  521. } else {
  522. ackPkt := h.packetBuilder.BuildAck(pkt.SeqNum, pkt.Version)
  523. if err := h.sender.SendPacket(addr, ackPkt); err != nil {
  524. return err
  525. }
  526. }
  527. // Get full user info for extended fields
  528. user, err := h.service.GetFullUserInfo(ctx, targetUIN)
  529. if err != nil {
  530. h.logger.Debug("failed to get full user info", "err", err)
  531. return nil
  532. }
  533. // Build and send SRV_EXT_INFO_REPLY (0x0122)
  534. wireInfo := &LegacyUserInfo{
  535. UIN: targetUIN,
  536. City: truncateField(user.ICQInfo.Basic.City, 64, h.logger, "city", targetUIN),
  537. State: truncateField(user.ICQInfo.Basic.State, 64, h.logger, "state", targetUIN),
  538. Country: user.ICQInfo.Basic.CountryCode,
  539. Phone: truncateField(user.ICQInfo.Basic.CellPhone, 30, h.logger, "phone", targetUIN),
  540. Homepage: truncateField(user.ICQInfo.More.HomePageAddr, 127, h.logger, "homepage", targetUIN),
  541. About: truncateField(user.ICQInfo.Notes.Notes, 450, h.logger, "about", targetUIN),
  542. Age: user.Age(time.Now),
  543. Gender: uint8(user.ICQInfo.More.Gender),
  544. }
  545. if session != nil {
  546. replyPkt := BuildV2ExtInfoReply(session.NextServerSeqNum(), seqPrefix, wireInfo)
  547. replyPkt.Version = session.Version
  548. return h.sender.SendToSession(session, MarshalV2ServerPacket(replyPkt))
  549. }
  550. // No session (first-login flow) — use seqPrefix as connection ID and send to addr
  551. replyPkt := BuildV2ExtInfoReply(seqPrefix, seqPrefix, wireInfo)
  552. return h.sender.SendPacket(addr, MarshalV2ServerPacket(replyPkt))
  553. }
  554. // handleSearchUIN processes a search by UIN request
  555. // Client sends: SEQ(2) + UIN(4) as data (from center-1.10.7 icq_SendSearchUINReq)
  556. //
  557. // IMPORTANT: This is an "extended event" in the client - ACK must be sent
  558. // first to move the event from running to extended list.
  559. func (h *V2Handler) handleSearchUIN(session *LegacySession, pkt *V2ClientPacket) error {
  560. if session == nil {
  561. return nil
  562. }
  563. ctx := context.Background()
  564. // Send ACK first - required for extended events
  565. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  566. return err
  567. }
  568. // Client sends SEQ(2) + UIN(4) = 6 bytes minimum
  569. if len(pkt.Data) < 6 {
  570. return h.sendSearchResult(session, &LegacyUserSearchResult{}, true, 0)
  571. }
  572. // Read client sub-sequence (2 bytes) and UIN (4 bytes)
  573. clientSubSeq := binary.LittleEndian.Uint16(pkt.Data[0:2])
  574. targetUIN := binary.LittleEndian.Uint32(pkt.Data[2:6])
  575. h.logger.Debug("search by UIN",
  576. "from", session.UIN,
  577. "target", targetUIN,
  578. )
  579. // Search for user
  580. result, err := h.service.SearchByUIN(ctx, targetUIN)
  581. if err != nil {
  582. h.logger.Debug("search failed", "err", err)
  583. // Send empty search result
  584. return h.sendSearchResult(session, &LegacyUserSearchResult{}, true, clientSubSeq)
  585. }
  586. return h.sendSearchResult(session, result, true, clientSubSeq)
  587. }
  588. // handleSearchUser processes a search by name/email request
  589. // Client sends: SEQ(2) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL
  590. // (from center-1.10.7 icq_SendSearchReq)
  591. //
  592. // IMPORTANT: This is an "extended event" in the client - ACK must be sent
  593. // first to move the event from running to extended list.
  594. func (h *V2Handler) handleSearchUser(session *LegacySession, pkt *V2ClientPacket) error {
  595. if session == nil {
  596. return nil
  597. }
  598. ctx := context.Background()
  599. // Send ACK first - required for extended events
  600. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  601. return err
  602. }
  603. h.logger.Debug("search by name/email", "from", session.UIN)
  604. // Need at least SEQ(2) + one length-prefixed string
  605. if len(pkt.Data) < 4 {
  606. return h.sendSearchResult(session, &LegacyUserSearchResult{}, true, 0)
  607. }
  608. // Read client sub-sequence
  609. clientSubSeq := binary.LittleEndian.Uint16(pkt.Data[0:2])
  610. // Skip SEQ prefix (2 bytes)
  611. r := bytes.NewReader(pkt.Data[2:])
  612. nick, _ := ParseLegacyString(r, true)
  613. first, _ := ParseLegacyString(r, true)
  614. last, _ := ParseLegacyString(r, true)
  615. email, _ := ParseLegacyString(r, true)
  616. h.logger.Debug("search params",
  617. "nick", nick,
  618. "first", first,
  619. "last", last,
  620. "email", email,
  621. )
  622. results, err := h.service.SearchByName(ctx, nick, first, last, email)
  623. if err != nil {
  624. h.logger.Debug("search failed", "err", err)
  625. return h.sendSearchResult(session, &LegacyUserSearchResult{}, true, clientSubSeq)
  626. }
  627. // Send each result
  628. for i, result := range results {
  629. isLast := i == len(results)-1
  630. r := result // copy for pointer
  631. if err := h.sendSearchResult(session, &r, isLast, clientSubSeq); err != nil {
  632. return err
  633. }
  634. }
  635. // If no results, send empty done
  636. if len(results) == 0 {
  637. return h.sendSearchResult(session, &LegacyUserSearchResult{}, true, clientSubSeq)
  638. }
  639. return nil
  640. }
  641. // handleOfflineMsgReq processes an offline message request
  642. //
  643. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  644. // receives SRV_ACK with the matching sequence number. We must send ACK first.
  645. // Then offline messages must use SRV_SYS_MSG_OFFLINE (0x00DC) with timestamp
  646. // fields, NOT SRV_SYS_MSG_ONLINE (0x0104). The client parses the timestamp
  647. // from 0x00DC packets to show when the message was originally sent.
  648. func (h *V2Handler) handleOfflineMsgReq(session *LegacySession, pkt *V2ClientPacket) error {
  649. if session == nil {
  650. return nil
  651. }
  652. ctx := context.Background()
  653. // Send ACK first to stop client retransmission
  654. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  655. return err
  656. }
  657. h.logger.Debug("offline message request", "uin", session.UIN)
  658. // Get offline messages
  659. messages, err := h.service.GetOfflineMessages(ctx, session.UIN)
  660. if err != nil {
  661. h.logger.Debug("failed to get offline messages", "err", err)
  662. }
  663. // Send each offline message using SRV_SYS_MSG_OFFLINE (0x00DC) with timestamp
  664. for _, msg := range messages {
  665. ts := msg.Timestamp
  666. if ts.IsZero() {
  667. ts = time.Now()
  668. }
  669. offlinePkt := BuildV2OfflineMessage(session.NextServerSeqNum(), msg.FromUIN, msg.MsgType, msg.Message, ts)
  670. offlinePkt.Version = session.Version
  671. if err := h.sender.SendToSession(session, MarshalV2ServerPacket(offlinePkt)); err != nil {
  672. h.logger.Debug("failed to send offline message", "err", err)
  673. }
  674. }
  675. // Send end of offline messages (SRV_SYS_MSG_DONE 0x00E6)
  676. // V2 format includes UIN(4) — from licq: 02 00 E6 00 04 00 50 A5 82 00
  677. uinData := make([]byte, 4)
  678. binary.LittleEndian.PutUint32(uinData[0:4], session.UIN)
  679. endPkt := &V2ServerPacket{
  680. Version: session.Version,
  681. Command: ICQLegacySrvSysMsgDone,
  682. SeqNum: session.NextServerSeqNum(),
  683. Data: uinData,
  684. }
  685. return h.sender.SendToSession(session, MarshalV2ServerPacket(endPkt))
  686. }
  687. // handleOfflineMsgAck processes an offline message acknowledgment
  688. //
  689. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  690. // receives SRV_ACK with the matching sequence number. We must send ACK first,
  691. // then do the work (delete offline messages).
  692. func (h *V2Handler) handleOfflineMsgAck(session *LegacySession, pkt *V2ClientPacket) error {
  693. if session == nil {
  694. return nil
  695. }
  696. ctx := context.Background()
  697. // Send ACK first to stop client retransmission
  698. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  699. return err
  700. }
  701. h.logger.Debug("offline message ack", "uin", session.UIN)
  702. // Delete offline messages
  703. if err := h.service.AckOfflineMessages(ctx, session.UIN); err != nil {
  704. h.logger.Debug("failed to ack offline messages", "err", err)
  705. }
  706. return nil
  707. }
  708. // handleVisibleList processes a visible list update
  709. func (h *V2Handler) handleVisibleList(session *LegacySession, pkt *V2ClientPacket) error {
  710. if session == nil {
  711. return nil
  712. }
  713. contactList, err := ParseV2ContactList(pkt.Data)
  714. if err != nil {
  715. h.logger.Debug("failed to parse visible list", "err", err)
  716. return h.sendAck(session, pkt.SeqNum)
  717. }
  718. h.logger.Debug("visible list received",
  719. "uin", session.UIN,
  720. "count", len(contactList.UINs),
  721. )
  722. session.SetVisibleList(contactList.UINs)
  723. return h.sendAck(session, pkt.SeqNum)
  724. }
  725. // handleInvisibleList processes an invisible list update
  726. func (h *V2Handler) handleInvisibleList(session *LegacySession, pkt *V2ClientPacket) error {
  727. if session == nil {
  728. return nil
  729. }
  730. contactList, err := ParseV2ContactList(pkt.Data)
  731. if err != nil {
  732. h.logger.Debug("failed to parse invisible list", "err", err)
  733. return h.sendAck(session, pkt.SeqNum)
  734. }
  735. h.logger.Debug("invisible list received",
  736. "uin", session.UIN,
  737. "count", len(contactList.UINs),
  738. )
  739. session.SetInvisibleList(contactList.UINs)
  740. return h.sendAck(session, pkt.SeqNum)
  741. }
  742. // handleUserAdd processes a user add request
  743. //
  744. // IMPORTANT: The client uses SendExpectEvent() which retransmits until it
  745. // receives SRV_ACK with the matching sequence number. We must send ACK first,
  746. // then do the work (add to contact list, check online status, send notification).
  747. func (h *V2Handler) handleUserAdd(session *LegacySession, pkt *V2ClientPacket) error {
  748. if session == nil {
  749. return nil
  750. }
  751. // Send ACK first to stop client retransmission
  752. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  753. return err
  754. }
  755. if len(pkt.Data) < 4 {
  756. return nil
  757. }
  758. targetUIN := binary.LittleEndian.Uint32(pkt.Data[0:4])
  759. h.logger.Debug("user add",
  760. "from", session.UIN,
  761. "target", targetUIN,
  762. )
  763. // Add to contact list
  764. contacts := session.GetContactList()
  765. contacts = append(contacts, targetUIN)
  766. session.SetContactList(contacts)
  767. ctx := context.Background()
  768. // Sync to clientSideBuddyList so OSCAR's BuddyArrived reaches this user
  769. // for the newly added contact (mirrors ProcessContactList sync logic)
  770. if _, err := h.service.ProcessUserAdd(ctx, session.Instance, UserAddRequest{
  771. FromUIN: session.UIN,
  772. TargetUIN: targetUIN,
  773. }); err != nil {
  774. h.logger.Debug("user add service call failed", "err", err)
  775. }
  776. // Check if target is online via legacy session and send notification
  777. targetSession := h.sessions.GetSession(targetUIN)
  778. if targetSession != nil {
  779. if h.dispatcher != nil {
  780. _ = h.dispatcher.SendUserOnline(session, targetUIN, targetSession.GetStatus())
  781. } else {
  782. _ = h.sendUserOnline(session, targetUIN, targetSession.GetStatus(), nil, 0)
  783. }
  784. // Also send the adder's online status to the target.
  785. // The target won't see the adder as online unless we tell them.
  786. if h.dispatcher != nil {
  787. _ = h.dispatcher.SendUserOnline(targetSession, session.UIN, session.GetStatus())
  788. } else {
  789. _ = h.sendUserOnline(targetSession, session.UIN, session.GetStatus(), nil, 0)
  790. }
  791. } else {
  792. // Check if target is online via OSCAR session
  793. info, err := h.service.GetUserInfoForProtocol(ctx, targetUIN)
  794. if err == nil && info != nil && info.Online {
  795. status := downgradeStatusForV2(info.Status)
  796. _ = h.sendUserOnline(session, targetUIN, status, nil, 0)
  797. }
  798. }
  799. return nil
  800. }
  801. // handleFirstLogin processes the initial login setup packet (0x04EC)
  802. // This is sent before the actual login to set up the connection.
  803. // From iserverd v3_process_firstlog(): just send ACK, nothing else.
  804. // The client then proceeds with getdeps (0x03F2).
  805. func (h *V2Handler) handleFirstLogin(session *LegacySession, addr *net.UDPAddr, pkt *V2ClientPacket) error {
  806. h.logger.Debug("first login packet received",
  807. "uin", pkt.UIN,
  808. "addr", addr.String(),
  809. )
  810. // Send V2-format ACK only. Do NOT send cmd 370 (server info reply).
  811. // The client's connection list is preserved, allowing the subsequent
  812. // cmd 1010 ACK to trigger message 0x455 via sub_430097, which advances
  813. // the wizard to the next step.
  814. ackPkt := &V2ServerPacket{
  815. Version: pkt.Version,
  816. Command: ICQLegacySrvAck,
  817. SeqNum: pkt.SeqNum,
  818. }
  819. return h.sender.SendPacket(addr, MarshalV2ServerPacket(ackPkt))
  820. }
  821. // handleGetDeps processes the pre-auth pseudo-login packet (0x03F2).
  822. // Historically called "get departments list" in iserverd (from its Users_Deps database table).
  823. // The server validates credentials, sends V3-format ACK, then sends the pre-auth response (0x0032).
  824. // The client then proceeds with the actual login (0x03E8).
  825. //
  826. // IMPORTANT: Even though the client sends version=2 in the header, this packet
  827. // uses V3 packet structure (12-byte header with seq1, seq2, UIN). The response
  828. // packets (ACK and depslist) MUST also be V3 format, matching iserverd behavior.
  829. // iserverd's v3_send_ack and v3_send_depslist always use V3_PROTO (0x0003) in
  830. // the response regardless of the client's declared version.
  831. //
  832. // Raw packet layout (V3 header):
  833. //
  834. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + DATA...
  835. //
  836. // Data layout:
  837. //
  838. // PWD_LEN(2) + PASSWORD(pwdLen) + STATUS(4)
  839. func (h *V2Handler) handleGetDeps(addr *net.UDPAddr, packet []byte) error {
  840. ctx := context.Background()
  841. if len(packet) < 14 { // 12-byte V3 header + at least 2 bytes for pwd_len
  842. h.logger.Debug("getdeps packet too short", "len", len(packet))
  843. return nil
  844. }
  845. // Parse as V3 header (12 bytes)
  846. seq1 := binary.LittleEndian.Uint16(packet[4:6])
  847. seq2 := binary.LittleEndian.Uint16(packet[6:8])
  848. uin := binary.LittleEndian.Uint32(packet[8:12])
  849. data := packet[12:]
  850. h.logger.Debug("getdeps packet (0x03F2)",
  851. "addr", addr.String(),
  852. "seq1", seq1,
  853. "seq2", seq2,
  854. "uin", uin,
  855. "data_len", len(data),
  856. "data_hex", fmt.Sprintf("%X", data),
  857. )
  858. // Parse password from data
  859. offset := 0
  860. if len(data) < 2 {
  861. return nil
  862. }
  863. pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  864. offset += 2
  865. if pwdLen == 0 || pwdLen > 20 || offset+int(pwdLen) > len(data) {
  866. h.logger.Debug("invalid password in getdeps", "pwd_len", pwdLen)
  867. return h.sendBadPassword(addr, seq1, 2)
  868. }
  869. password := string(data[offset : offset+int(pwdLen)])
  870. if len(password) > 0 && password[len(password)-1] == 0 {
  871. password = password[:len(password)-1]
  872. }
  873. h.logger.Info("getdeps (pseudo-login)",
  874. "uin", uin,
  875. "password_len", len(password),
  876. )
  877. // Validate credentials using service layer
  878. authReq := AuthRequest{
  879. UIN: uin,
  880. Password: password,
  881. Version: ICQLegacyVersionV2,
  882. }
  883. loginOK, err := h.service.ValidateCredentials(ctx, authReq.UIN, authReq.Password)
  884. if err != nil || !loginOK {
  885. h.logger.Info("getdeps failed - invalid credentials", "uin", uin)
  886. return h.sendBadPassword(addr, seq1, 2)
  887. }
  888. h.logger.Debug("credentials validated successfully", "uin", uin)
  889. // Send V2-format ACK for cmd 1010.
  890. ackPkt := h.packetBuilder.BuildAck(seq1, ICQLegacyVersionV2)
  891. if err := h.sender.SendPacket(addr, ackPkt); err != nil {
  892. return err
  893. }
  894. // Send V2-format DeptsList (cmd 0x0032 = 50).
  895. // From disassembly: the client's cmd 50 handler reads:
  896. // connection_id (word) + UIN (dword)
  897. // The connection_id must match the one used by cmd 1010 (= seq2).
  898. // The UIN must match g_saved_uin + 0x9C.
  899. depsData := make([]byte, 6)
  900. binary.LittleEndian.PutUint16(depsData[0:2], seq2) // connection ID
  901. binary.LittleEndian.PutUint32(depsData[2:6], uin) // UIN
  902. depsPkt := &V2ServerPacket{
  903. Version: ICQLegacyVersionV2,
  904. Command: ICQLegacySrvUserDepsList, // 0x0032
  905. SeqNum: seq1,
  906. Data: depsData,
  907. }
  908. return h.sender.SendPacket(addr, MarshalV2ServerPacket(depsPkt))
  909. }
  910. // handleAuthorize processes CMD_AUTHORIZE (0x0456) which is a thru-server
  911. // message in V2. The packet format is identical to CMD_THRUxSERVER (0x010E):
  912. // TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  913. // iserverd routes both 0x010E and 0x0456 through the same v3_process_sysmsg.
  914. func (h *V2Handler) handleAuthorize(session *LegacySession, pkt *V2ClientPacket) error {
  915. return h.handleSendMessage(session, pkt)
  916. }
  917. // handleUpdateBasic processes a basic profile update (CMD_UPDATExBASIC 0x04A6)
  918. // Client sends: SEQ(2) + ALIAS_LEN(2) + ALIAS + FIRST_LEN(2) + FIRST +
  919. //
  920. // LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
  921. //
  922. // Server responds with SRV_UPDATEDxBASIC (0x00B4) containing SEQ(2) on success,
  923. // or SRV_UPDATExBASICxFAIL (0x00BE) on failure.
  924. //
  925. // IMPORTANT: This is an "extended event" - ACK moves it from running to
  926. // extended list, then the response completes it via DoneExtendedEvent().
  927. func (h *V2Handler) handleUpdateBasic(session *LegacySession, pkt *V2ClientPacket) error {
  928. if session == nil {
  929. return nil
  930. }
  931. // Send ACK first - required for extended events
  932. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  933. return err
  934. }
  935. // Need at least SEQ(2) + one length-prefixed string
  936. if len(pkt.Data) < 4 {
  937. return h.sendUpdateBasicFail(session, 0)
  938. }
  939. // Read the update sequence (2 bytes)
  940. updateSeq := binary.LittleEndian.Uint16(pkt.Data[0:2])
  941. r := bytes.NewReader(pkt.Data[2:])
  942. alias, _ := ParseLegacyString(r, true)
  943. firstName, _ := ParseLegacyString(r, true)
  944. lastName, _ := ParseLegacyString(r, true)
  945. email, _ := ParseLegacyString(r, true)
  946. var auth uint8
  947. hasAuth := binary.Read(r, binary.LittleEndian, &auth) == nil && r.Len() >= 0
  948. h.logger.Debug("update basic info",
  949. "uin", session.UIN,
  950. "alias", alias,
  951. "first", firstName,
  952. "last", lastName,
  953. "email", email,
  954. "auth", auth,
  955. "has_auth", hasAuth,
  956. )
  957. // Persist basic info via service layer using read-merge-write
  958. // to avoid overwriting fields not present in the V2 packet
  959. // (city, state, country, phone, etc.)
  960. ctx := context.Background()
  961. existing, err := h.service.GetFullUserInfo(ctx, session.UIN)
  962. var info state.ICQBasicInfo
  963. if err == nil && existing != nil {
  964. info = existing.ICQInfo.Basic
  965. }
  966. info.Nickname = alias
  967. info.FirstName = firstName
  968. info.LastName = lastName
  969. info.EmailAddress = email
  970. if err := h.service.UpdateBasicInfo(ctx, session.UIN, info); err != nil {
  971. h.logger.Error("V2 update basic info failed", "uin", session.UIN, "err", err)
  972. }
  973. // Update auth mode only if the auth byte was present in the packet.
  974. // V2 format (0x04A6) includes AUTH(1), V4 format (0x050A) does not.
  975. if hasAuth && pkt.Command == ICQLegacyCmdUpdateBasic {
  976. if err := h.service.SetAuthMode(ctx, session.UIN, auth == 1); err != nil {
  977. h.logger.Error("V2 set auth mode failed", "uin", session.UIN, "err", err)
  978. }
  979. }
  980. // Send SRV_UPDATEDxBASIC — V2 uses 0x00B4, V4 uses 0x01E0
  981. replyCmd := ICQLegacySrvUpdatedBasic // 0x00B4
  982. if pkt.Command == ICQLegacyCmdSetBasicInfo {
  983. replyCmd = ICQLegacySrvUpdatedBasicV4 // 0x01E0
  984. }
  985. data := make([]byte, 2)
  986. binary.LittleEndian.PutUint16(data[0:2], updateSeq)
  987. replyPkt := &V2ServerPacket{
  988. Version: session.Version,
  989. Command: replyCmd,
  990. SeqNum: session.NextServerSeqNum(),
  991. Data: data,
  992. }
  993. return h.sender.SendToSession(session, MarshalV2ServerPacket(replyPkt))
  994. }
  995. // sendUpdateBasicFail sends SRV_UPDATExBASICxFAIL (0x00BE)
  996. func (h *V2Handler) sendUpdateBasicFail(session *LegacySession, updateSeq uint16) error {
  997. data := make([]byte, 2)
  998. binary.LittleEndian.PutUint16(data[0:2], updateSeq)
  999. pkt := &V2ServerPacket{
  1000. Version: session.Version,
  1001. Command: ICQLegacySrvUpdateBasicFail,
  1002. SeqNum: session.NextServerSeqNum(),
  1003. Data: data,
  1004. }
  1005. return h.sender.SendToSession(session, MarshalV2ServerPacket(pkt))
  1006. }
  1007. // handleUpdateDetail processes an extended profile update (CMD_UPDATExDETAIL 0x04B0)
  1008. // Client sends: SEQ(2) + CITY_LEN(2) + CITY + COUNTRY(2) + COUNTRY_STAT(1) +
  1009. //
  1010. // STATE_LEN(2) + STATE + AGE(2) + SEX(1) + PHONE_LEN(2) + PHONE +
  1011. // HP_LEN(2) + HP + ABOUT_LEN(2) + ABOUT
  1012. //
  1013. // Server responds with SRV_UPDATEDxDETAIL (0x00C8) containing SEQ(2) on success,
  1014. // or SRV_UPDATExDETAILxFAIL (0x00D2) on failure.
  1015. //
  1016. // IMPORTANT: This is an "extended event" - ACK moves it from running to
  1017. // extended list, then the response completes it via DoneExtendedEvent().
  1018. func (h *V2Handler) handleUpdateDetail(session *LegacySession, pkt *V2ClientPacket) error {
  1019. if session == nil {
  1020. return nil
  1021. }
  1022. // Send ACK first - required for extended events
  1023. if err := h.sendAck(session, pkt.SeqNum); err != nil {
  1024. return err
  1025. }
  1026. // Need at least SEQ(2) + one length-prefixed string
  1027. if len(pkt.Data) < 4 {
  1028. return h.sendUpdateDetailFail(session, 0)
  1029. }
  1030. // Read the update sequence (2 bytes)
  1031. updateSeq := binary.LittleEndian.Uint16(pkt.Data[0:2])
  1032. r := bytes.NewReader(pkt.Data[2:])
  1033. city, _ := ParseLegacyString(r, true)
  1034. var country uint16
  1035. _ = binary.Read(r, binary.LittleEndian, &country)
  1036. var countryStat uint8
  1037. _ = binary.Read(r, binary.LittleEndian, &countryStat)
  1038. st, _ := ParseLegacyString(r, true)
  1039. var age uint16
  1040. _ = binary.Read(r, binary.LittleEndian, &age)
  1041. var sex uint8
  1042. _ = binary.Read(r, binary.LittleEndian, &sex)
  1043. phone, _ := ParseLegacyString(r, true)
  1044. homepage, _ := ParseLegacyString(r, true)
  1045. about, _ := ParseLegacyString(r, true)
  1046. h.logger.Debug("update detail info",
  1047. "uin", session.UIN,
  1048. "city", city,
  1049. "country", country,
  1050. "state", st,
  1051. "age", age,
  1052. "sex", sex,
  1053. "phone", phone,
  1054. "homepage", homepage,
  1055. "about", about,
  1056. )
  1057. // Persist detail info via service layer
  1058. // Read existing basic info to avoid overwriting nick/first/last/email
  1059. ctx := context.Background()
  1060. existing, err := h.service.GetFullUserInfo(ctx, session.UIN)
  1061. if err == nil && existing != nil {
  1062. existing.ICQInfo.Basic.City = city
  1063. existing.ICQInfo.Basic.CountryCode = country
  1064. existing.ICQInfo.Basic.State = st
  1065. existing.ICQInfo.Basic.Phone = phone
  1066. if err := h.service.UpdateBasicInfo(ctx, session.UIN, existing.ICQInfo.Basic); err != nil {
  1067. h.logger.Error("V2 update detail basic failed", "uin", session.UIN, "err", err)
  1068. }
  1069. }
  1070. // Read existing more info to avoid overwriting birthday/languages
  1071. if existing != nil {
  1072. existing.ICQInfo.More.Gender = uint16(sex)
  1073. existing.ICQInfo.More.HomePageAddr = homepage
  1074. if err := h.service.UpdateMoreInfo(ctx, session.UIN, existing.ICQInfo.More); err != nil {
  1075. h.logger.Error("V2 update detail more failed", "uin", session.UIN, "err", err)
  1076. }
  1077. }
  1078. if about != "" {
  1079. if err := h.service.SetNotes(ctx, session.UIN, about); err != nil {
  1080. h.logger.Error("V2 update detail about failed", "uin", session.UIN, "err", err)
  1081. }
  1082. }
  1083. // Send SRV_UPDATEDxDETAIL (0x00C8) with the update sequence
  1084. data := make([]byte, 2)
  1085. binary.LittleEndian.PutUint16(data[0:2], updateSeq)
  1086. replyPkt := &V2ServerPacket{
  1087. Version: session.Version,
  1088. Command: ICQLegacySrvUpdatedDetail,
  1089. SeqNum: session.NextServerSeqNum(),
  1090. Data: data,
  1091. }
  1092. return h.sender.SendToSession(session, MarshalV2ServerPacket(replyPkt))
  1093. }
  1094. // sendUpdateDetailFail sends SRV_UPDATExDETAILxFAIL (0x00D2)
  1095. func (h *V2Handler) sendUpdateDetailFail(session *LegacySession, updateSeq uint16) error {
  1096. data := make([]byte, 2)
  1097. binary.LittleEndian.PutUint16(data[0:2], updateSeq)
  1098. pkt := &V2ServerPacket{
  1099. Version: session.Version,
  1100. Command: ICQLegacySrvUpdateDetailFail,
  1101. SeqNum: session.NextServerSeqNum(),
  1102. Data: data,
  1103. }
  1104. return h.sender.SendToSession(session, MarshalV2ServerPacket(pkt))
  1105. }
  1106. // handleRegNewUser processes a CMD_REG_NEW_USER (0x03FC) registration packet.
  1107. // This uses the srv_net_icq_pak format (6-byte header, no UIN).
  1108. // Data format from center icq_RegNewUser():
  1109. //
  1110. // CONST(2) + PWD_LEN(2) + PASSWORD(variable, null-terminated) + TRAILING(8)
  1111. //
  1112. // Server responds with SRV_NEW_UIN (0x0046) containing the new UIN.
  1113. func (h *V2Handler) handleRegNewUser(addr *net.UDPAddr, pkt *V2ClientPacket) error {
  1114. ctx := context.Background()
  1115. h.logger.Debug("registration packet (0x03FC)",
  1116. "addr", addr.String(),
  1117. "data_len", len(pkt.Data),
  1118. "data_hex", fmt.Sprintf("%X", pkt.Data),
  1119. )
  1120. // Need at least: CONST(2) + PWD_LEN(2) + 1 byte password + null
  1121. if len(pkt.Data) < 6 {
  1122. h.logger.Debug("registration packet too short")
  1123. return nil
  1124. }
  1125. offset := 2 // skip 2-byte constant
  1126. // Read password length (2 bytes)
  1127. pwdLen := binary.LittleEndian.Uint16(pkt.Data[offset : offset+2])
  1128. offset += 2
  1129. if pwdLen == 0 || len(pkt.Data) < offset+int(pwdLen) {
  1130. h.logger.Debug("invalid password length in registration", "pwd_len", pwdLen)
  1131. return nil
  1132. }
  1133. // Read password, strip null terminator
  1134. password := string(pkt.Data[offset : offset+int(pwdLen)])
  1135. if len(password) > 0 && password[len(password)-1] == 0 {
  1136. password = password[:len(password)-1]
  1137. }
  1138. h.logger.Info("registration attempt",
  1139. "addr", addr.String(),
  1140. "password_len", len(password),
  1141. )
  1142. // Call service layer to create the new user
  1143. newUIN, err := h.service.RegisterNewUser(ctx, "", "", "", "", password)
  1144. if err != nil {
  1145. h.logger.Error("registration failed", "err", err)
  1146. return nil
  1147. }
  1148. h.logger.Info("registration successful",
  1149. "new_uin", newUIN,
  1150. "addr", addr.String(),
  1151. )
  1152. // Send SRV_NEW_UIN (0x0046) response
  1153. replyPkt := BuildV2NewUIN(pkt.SeqNum, newUIN)
  1154. replyBytes := MarshalV2ServerPacket(replyPkt)
  1155. h.logger.Debug("sending SRV_NEW_UIN",
  1156. "new_uin", newUIN,
  1157. "seq", pkt.SeqNum,
  1158. "raw_hex", fmt.Sprintf("%X", replyBytes),
  1159. )
  1160. return h.sender.SendPacket(addr, replyBytes)
  1161. }