| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181 |
- package icq_legacy
- import (
- "bytes"
- "context"
- "encoding/binary"
- "fmt"
- "log/slog"
- "net"
- "github.com/mk6i/open-oscar-server/state"
- )
- // V3Handler handles ICQ V3 protocol packets
- // V3 packet format (from iserverd source):
- // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + DATA...
- // Total header: 12 bytes (NO checksum in V3, unlike V5!)
- type V3Handler struct {
- sessions *LegacySessionManager
- service LegacyService
- sender PacketSender
- packetBuilder V3PacketBuilder // Packet builder for constructing V3 protocol packets
- dispatcher MessageDispatcher // Central dispatcher for cross-protocol messaging
- logger *slog.Logger
- }
- // NewV3Handler creates a new V3 protocol handler
- func NewV3Handler(
- sessions *LegacySessionManager,
- service LegacyService,
- sender PacketSender,
- packetBuilder V3PacketBuilder,
- logger *slog.Logger,
- ) *V3Handler {
- return &V3Handler{
- sessions: sessions,
- service: service,
- sender: sender,
- packetBuilder: packetBuilder,
- dispatcher: nil, // Set later via SetDispatcher to avoid circular dependency
- logger: logger,
- }
- }
- // SetSender sets the packet sender (for circular dependency resolution)
- func (h *V3Handler) SetSender(sender PacketSender) {
- h.sender = sender
- }
- // SetDispatcher sets the message dispatcher for cross-protocol messaging
- func (h *V3Handler) SetDispatcher(dispatcher MessageDispatcher) {
- h.dispatcher = dispatcher
- }
- // Handle processes a V3 protocol packet
- func (h *V3Handler) Handle(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
- // V3 packet format (from iserverd source):
- // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + DATA...
- // Total header: 12 bytes (NO checksum in V3!)
- h.logger.Debug("raw V3 packet",
- "hex", fmt.Sprintf("%X", packet),
- "len", len(packet),
- )
- if len(packet) < 12 {
- return fmt.Errorf("V3 packet too short: %d bytes", len(packet))
- }
- // Parse V3 header directly (iserverd: pack >> vvers >> pcomm >> seq1 >> seq2 >> uin_num)
- version := binary.LittleEndian.Uint16(packet[0:2])
- command := binary.LittleEndian.Uint16(packet[2:4])
- seq1 := binary.LittleEndian.Uint16(packet[4:6])
- seq2 := binary.LittleEndian.Uint16(packet[6:8])
- uin := binary.LittleEndian.Uint32(packet[8:12])
- // Extract data after header
- var data []byte
- if len(packet) > 12 {
- data = packet[12:]
- }
- h.logger.Debug("V3 packet parsed",
- "version", version,
- "command", fmt.Sprintf("0x%04X", command),
- "seq1", seq1,
- "seq2", seq2,
- "uin", uin,
- "data_len", len(data),
- )
- // Update session activity if we have one
- if session != nil {
- session.UpdateActivity()
- session.SeqNumClient = seq1
- }
- // If no session exists and this is not a login/registration/ack command,
- // send NOT_CONNECTED to force the client to reconnect.
- if session == nil {
- switch command {
- case ICQLegacyCmdAck, ICQLegacyCmdFirstLogin,
- ICQLegacyCmdGetDeps, ICQLegacyCmdLogin:
- // Allow these through - they don't require a session
- default:
- h.logger.Info("V3 packet from unknown session, sending NOT_CONNECTED",
- "command", fmt.Sprintf("0x%04X", command),
- "uin", uin,
- "addr", addr.String(),
- )
- return h.sendNotConnected(addr, seq2, uin)
- }
- }
- // Handle V3 commands directly (matching iserverd's handle_v3_proto)
- switch command {
- case ICQLegacyCmdFirstLogin:
- return h.handleFirstLogin(addr, seq1, seq2, uin)
- case ICQLegacyCmdLogin:
- return h.handleLogin(session, addr, seq1, seq2, uin, data)
- case ICQLegacyCmdGetDeps:
- return h.handleGetDeps(addr, seq1, seq2, data)
- case ICQLegacyCmdUserGetInfo:
- return h.handleGetInfo(session, seq1, seq2, uin, data)
- case ICQLegacyCmdAck:
- h.logger.Debug("received ACK", "seq1", seq1, "seq2", seq2)
- return nil
- case ICQLegacyCmdKeepAlive, ICQLegacyCmdKeepAlive2:
- return h.handlePing(session, addr, seq1, seq2, uin)
- case ICQLegacyCmdLogoff:
- return h.handleLogoff(session, seq1, seq2, uin)
- case ICQLegacyCmdContactList:
- return h.handleContactList(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSetStatus:
- return h.handleSetStatus(session, seq1, seq2, uin, data)
- case ICQLegacyCmdThruServer, ICQLegacyCmdAuthorize: // 0x010E and 0x0456 (authorize)
- return h.handleMessage(session, seq1, seq2, uin, data)
- case ICQLegacyCmdUserAdd:
- return h.handleUserAdd(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSysMsgReq:
- return h.handleOfflineMsgReq(session, seq1, seq2, uin)
- case ICQLegacyCmdSetBasicInfo:
- return h.handleSetBasicInfo(session, seq1, seq2, uin, data)
- case 0x0582: // SetHomeInfo
- return h.handleSetHomeInfo(session, seq1, seq2, uin, data)
- case 0x058C: // SetHomeWeb
- return h.handleSetHomeWeb(session, seq1, seq2, uin, data)
- case 0x0578: // SetWorkInfo
- return h.handleSetWorkInfo(session, seq1, seq2, uin, data)
- case 0x05BE: // SetWorkWeb
- return h.handleSetWorkWeb(session, seq1, seq2, uin, data)
- case 0x0604: // ICQ_CMDxRCV_UKNOWN_DEP - unknown deps request (historical iserverd naming)
- return h.handleUnknownDep(session, seq1, seq2, uin, data)
- case 0x06AE: // ICQ_CMDxRCV_VISxLIST - visible list
- return h.handleVisibleList(session, seq1, seq2, uin, data)
- case 0x06A4: // ICQ_CMDxRCV_INVISxLIST - invisible list
- return h.handleInvisibleList(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSearchStart: // 0x05C8 - Search user
- return h.handleSearchStart(session, seq1, seq2, uin, data)
- case ICQLegacyCmdGetDeps1: // 0x05F0 - Get deps list during session (pre-auth list, historical iserverd naming)
- return h.handleGetDeps1(session, seq1, seq2, uin, data)
- case 0x05AA: // ICQ_CMDxRCV_GETxNOTES - Get notes
- return h.handleGetNotes(session, seq1, seq2, uin, data)
- case 0x0596: // ICQ_CMDxRCV_SETxNOTES - Set notes
- return h.handleSetNotes(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSetPassword: // 0x049C - Set password
- return h.handleSetPassword(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSetAuth: // 0x0514 - Set auth mode
- return h.handleSetAuth(session, seq1, seq2, uin, data)
- case 0x0528: // ICQ_CMDxRCV_SETxSTATE - Set client state (not search!)
- return h.handleSetState(session, seq1, seq2, uin, data)
- case 0x0532: // ICQ_CMDxRCV_USAGExSTATS - Usage statistics
- return h.handleUsageStats(session, seq1, seq2, uin, data)
- case ICQLegacyCmdGetExternals: // 0x04C4 - Get externals
- return h.handleGetExternals(session, seq1, seq2, uin, data)
- case 0x0442: // ICQ_CMDxRCV_SYSxMSGxDONExACK - System message ack
- return h.handleSysAck(session, seq1, seq2, uin, data)
- case ICQLegacyCmdReconnect: // 0x015E - Reconnect/online info
- return h.handleOnlineInfo(session, seq1, seq2, uin, data)
- case 0x0460: // ICQ_CMDxRCV_USERxGETINFO1 - Get basic info only
- return h.handleGetInfo1(session, seq1, seq2, uin, data)
- default:
- h.logger.Debug("unhandled V3 command",
- "command", fmt.Sprintf("0x%04X", command),
- "uin", uin,
- )
- // Send ACK for unknown commands
- return h.sendAck(addr, seq1, seq2, uin)
- }
- }
- // handleFirstLogin processes the first login packet (0x04EC)
- // From iserverd: v3_process_firstlog() - just sends ACK
- func (h *V3Handler) handleFirstLogin(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- h.logger.Debug("first login packet",
- "uin", uin,
- "addr", addr.String(),
- )
- return h.sendAck(addr, seq1, seq2, uin)
- }
- // handleGetDeps processes the pre-auth pseudo-login packet (0x03F2).
- // Historically called "get departments list" in iserverd (from its Users_Deps database table).
- // Data format: JUNK(4) + UIN(4) + PWD_LEN(2) + PASSWORD
- // The server validates credentials and sends the pre-auth response before actual login.
- //
- // Refactored to use service layer (AuthenticateUser) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> build response
- func (h *V3Handler) handleGetDeps(addr *net.UDPAddr, seq1, seq2 uint16, data []byte) error {
- ctx := context.Background()
- // 1. Unmarshal packet to typed struct
- if len(data) < 10 {
- h.logger.Debug("getdeps packet too short", "len", len(data))
- return nil
- }
- offset := 0
- // Skip junk (4 bytes)
- offset += 4
- // Read UIN (4 bytes)
- uin := binary.LittleEndian.Uint32(data[offset : offset+4])
- offset += 4
- // Read password length (2 bytes)
- pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- if pwdLen > 20 {
- pwdLen = 20
- }
- // Read password
- if offset+int(pwdLen) > len(data) {
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- password := string(data[offset : offset+int(pwdLen)])
- if len(password) > 0 && password[len(password)-1] == 0 {
- password = password[:len(password)-1]
- }
- h.logger.Info("V3 getdeps (pseudo-login)",
- "uin", uin,
- "password_len", len(password),
- )
- // 2. Call service layer with typed request
- authReq := AuthRequest{
- UIN: uin,
- Password: password,
- Version: ICQLegacyVersionV3,
- }
- loginOK, err := h.service.ValidateCredentials(ctx, authReq.UIN, authReq.Password)
- if err != nil {
- h.logger.Error("getdeps authentication error", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- if !loginOK {
- h.logger.Info("getdeps failed - invalid credentials", "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- // 3. Build and send responses using packet builder
- // Send ACK
- _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // Send pre-auth response using packet builder
- return h.sender.SendPacket(addr, h.packetBuilder.BuildDeptsList(seq2, uin))
- }
- // handleLogin processes login packet (0x03E8)
- // From iserverd v3_process_login():
- // After header (12 bytes): TIMESTAMP(4) + TCP_PORT(4) + PWD_LEN(2) + PASSWORD + ...
- //
- // Refactored to use service layer (AuthenticateUser) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> build response
- func (h *V3Handler) handleLogin(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32, data []byte) error {
- ctx := context.Background()
- // First send ACK (iserverd does this immediately)
- _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 1. Unmarshal packet to typed struct
- // V3 login data format (from iserverd):
- // TIMESTAMP(4) + TCP_PORT(4) + PWD_LEN(2) + PASSWORD(pwdLen) + ...
- if len(data) < 10 {
- h.logger.Debug("login packet too short", "len", len(data))
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- offset := 0
- // Read timestamp (4 bytes) - skip it
- // timestamp := binary.LittleEndian.Uint32(data[offset : offset+4])
- offset += 4
- // Read TCP port (4 bytes)
- port := binary.LittleEndian.Uint32(data[offset : offset+4])
- offset += 4
- // Read password length (2 bytes)
- if offset+2 > len(data) {
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Sanity check password length
- if pwdLen > 20 {
- pwdLen = 20
- }
- // Read password
- if offset+int(pwdLen) > len(data) {
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- password := string(data[offset : offset+int(pwdLen)])
- // Remove null terminator if present
- if len(password) > 0 && password[len(password)-1] == 0 {
- password = password[:len(password)-1]
- }
- h.logger.Info("V3 login attempt",
- "uin", uin,
- "port", port,
- "password_len", len(password),
- )
- // 2. Call service layer with typed request
- authReq := AuthRequest{
- UIN: uin,
- Password: password,
- Status: ICQLegacyStatusOnline, // Default status for V3
- TCPPort: port,
- Version: ICQLegacyVersionV3,
- }
- authResult, err := h.service.AuthenticateUser(ctx, authReq)
- if err != nil {
- h.logger.Error("login authentication error", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- if !authResult.Success {
- h.logger.Info("login failed - invalid credentials",
- "uin", uin,
- "error_code", authResult.ErrorCode,
- )
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- // 3. Create session (handler responsibility - session management)
- // TODO: parse and store direct connection info (IP, port, DC version, DC type)
- // from the V3 login packet so that presence notifications can include real
- // connection details when ICQ_LEGACY_DIRECT_CONNECTIONS includes V3.
- newSession, err := h.sessions.CreateSession(uin, addr, ICQLegacyVersionV3, authResult.oscarSession)
- if err != nil {
- h.logger.Error("failed to create session", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- newSession.Password = password
- // 4. Build and send response using packet builder
- loginReplyPkt := h.packetBuilder.BuildLoginReply(newSession, seq1, seq2)
- if err := h.sender.SendToSession(newSession, loginReplyPkt); err != nil {
- return err
- }
- h.logger.Info("V3 login successful",
- "uin", uin,
- "session_id", newSession.SessionID,
- )
- if err := h.service.NotifyUserOnline(ctx, newSession.UIN, newSession.GetStatus()); err != nil {
- h.logger.Debug("V3 failed to notify OSCAR clients of online", "uin", newSession.UIN, "err", err)
- }
- return nil
- }
- // handlePing processes keep-alive packets
- func (h *V3Handler) handlePing(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- if session == nil {
- return h.sendNotConnected(addr, seq2, uin)
- }
- return h.sendAck(addr, seq1, seq2, uin)
- }
- // handleLogoff processes logout
- func (h *V3Handler) handleLogoff(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
- if session == nil {
- return nil
- }
- h.logger.Info("V3 logout", "uin", uin)
- // Notify contacts that this user is going offline BEFORE removing session
- h.notifyContactsUserOffline(session)
- // Notify OSCAR clients that this user went offline
- ctx := context.Background()
- if err := h.service.NotifyUserOffline(ctx, uin); err != nil {
- h.logger.Debug("V3 failed to notify OSCAR clients of offline", "uin", uin, "err", err)
- }
- h.sessions.RemoveSession(uin)
- return nil
- }
- // notifyContactsUserOffline notifies all contacts who have this user in their list that we're offline
- func (h *V3Handler) notifyContactsUserOffline(session *LegacySession) {
- if h.dispatcher == nil {
- return
- }
- // Find all sessions that have this user in their contact list
- contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
- h.logger.Debug("notifying contacts of user offline",
- "uin", session.UIN,
- "contacts_to_notify", contactsToNotify,
- )
- for _, contactUIN := range contactsToNotify {
- contactSession := h.sessions.GetSession(contactUIN)
- if contactSession != nil {
- // Use dispatcher to send offline notification in correct protocol format
- _ = h.dispatcher.SendUserOffline(contactSession, session.UIN)
- }
- }
- }
- // handleContactList processes contact list (0x0406)
- // From iserverd v3_process_contact_list(): processes contact list and sends online notifications
- //
- // Refactored to use service layer (ProcessContactList) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> send notifications
- func (h *V3Handler) handleContactList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to typed request struct
- req, err := h.parseContactListPacket(data, uin)
- if err != nil {
- h.logger.Debug("V3 contact list parse error", "uin", uin, "err", err)
- // Send contact list done even on parse error
- return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
- }
- h.logger.Debug("V3 contact list",
- "uin", uin,
- "count", len(req.Contacts),
- )
- // Store contact list in session (handler responsibility - session management)
- session.SetContactList(req.Contacts)
- // 3. Call service layer with typed request
- ctx := context.Background()
- result, err := h.service.ProcessContactList(ctx, session.Instance, req)
- if err != nil {
- h.logger.Error("contact list processing failed", "uin", uin, "err", err)
- // Still send contact list done on error
- return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
- }
- // 4. Send online notifications based on service result
- for _, contact := range result.OnlineContacts {
- if contact.Online {
- if h.dispatcher != nil {
- // Use central dispatcher - routes to correct protocol based on session's version
- _ = h.dispatcher.SendUserOnline(session, contact.UIN, contact.Status)
- } else {
- // Fallback to V3 format if dispatcher not set
- _ = h.sendUserOnline(session, contact.UIN, contact.Status)
- }
- }
- }
- h.notifyContactsUserOnline(session)
- // 5. Send contact list done using packet builder
- return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
- }
- // parseContactListPacket parses a V3 contact list packet into a typed ContactListRequest struct.
- // Format: TIMESTAMP(4) + COUNT(1) + UIN(4)*COUNT
- func (h *V3Handler) parseContactListPacket(data []byte, ownerUIN uint32) (ContactListRequest, error) {
- req := ContactListRequest{
- UIN: ownerUIN,
- Contacts: make([]uint32, 0),
- }
- if len(data) < 5 {
- return req, fmt.Errorf("contact list packet too short: %d bytes", len(data))
- }
- // Skip timestamp (4 bytes)
- offset := 4
- // Contact count (1 byte)
- count := int(data[offset])
- offset++
- // Parse contact UINs
- for i := 0; i < count && offset+4 <= len(data); i++ {
- contactUIN := binary.LittleEndian.Uint32(data[offset : offset+4])
- req.Contacts = append(req.Contacts, contactUIN)
- offset += 4
- }
- return req, nil
- }
- // notifyContactsUserOnline notifies all contacts who have this user in their list that we're online
- // Following iserverd's pattern: when user comes online, notify everyone who cares
- func (h *V3Handler) notifyContactsUserOnline(session *LegacySession) {
- if h.dispatcher == nil {
- return
- }
- // Find all sessions that have this user in their contact list
- contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
- h.logger.Debug("notifying contacts of user online",
- "uin", session.UIN,
- "contacts_to_notify", contactsToNotify,
- )
- for _, contactUIN := range contactsToNotify {
- contactSession := h.sessions.GetSession(contactUIN)
- if contactSession != nil {
- // Use dispatcher to send in correct protocol format for each contact
- _ = h.dispatcher.SendUserOnline(contactSession, session.UIN, session.GetStatus())
- }
- }
- }
- // handleSetStatus processes status change (0x04D8)
- // From iserverd v3_process_status(): when a user changes their status
- // (e.g., Online -> Away, Away -> DND), this handler updates the session
- // and notifies all contacts who have this user in their contact list.
- //
- // Refactored to use service layer (ProcessStatusChange) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> broadcast
- func (h *V3Handler) handleSetStatus(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- ctx := context.Background()
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to typed request struct
- // Parse status - format: TIMESTAMP(4) + STATUS(4)
- if len(data) < 8 {
- return nil
- }
- newStatus := binary.LittleEndian.Uint32(data[4:8])
- oldStatus := session.GetStatus()
- h.logger.Debug("V3 status change",
- "uin", uin,
- "old_status", fmt.Sprintf("0x%08X", oldStatus),
- "new_status", fmt.Sprintf("0x%08X", newStatus),
- )
- // Update session status (handler responsibility - session management)
- session.SetStatus(newStatus)
- // 3. Call service layer with typed request
- statusReq := StatusChangeRequest{
- UIN: uin,
- NewStatus: newStatus,
- OldStatus: oldStatus,
- }
- statusResult, err := h.service.ProcessStatusChange(ctx, statusReq)
- if err != nil {
- h.logger.Debug("failed to process status change", "err", err)
- return nil
- }
- h.logger.Debug("V3 status change processed",
- "uin", uin,
- "notify_count", len(statusResult.NotifyTargets),
- )
- // 4. Broadcast status change to contacts using packet builder
- // The service layer returns the list of users to notify
- for _, target := range statusResult.NotifyTargets {
- targetSession := h.sessions.GetSession(target.UIN)
- if targetSession != nil {
- if h.dispatcher != nil {
- // Use central dispatcher - routes to correct protocol based on target's version
- _ = h.dispatcher.SendStatusChange(targetSession, uin, newStatus)
- } else {
- // Fallback to V3 format if dispatcher not set
- statusPkt := h.packetBuilder.BuildUserStatus(
- targetSession.NextServerSeqNum(),
- uin,
- newStatus,
- )
- _ = h.sender.SendToSession(targetSession, statusPkt)
- }
- }
- }
- return nil
- }
- // handleMessage processes through-server messages (0x010E)
- // From iserverd v3_process_sysmsg(): forwards messages to target user
- // Format: TIMESTAMP(4) + TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- //
- // Refactored to use service layer (ProcessMessage) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> route
- func (h *V3Handler) handleMessage(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to typed request struct
- req, err := h.parseMessagePacket(data, uin)
- if err != nil {
- h.logger.Debug("V3 message parse error", "from", uin, "err", err)
- return nil
- }
- h.logger.Info("V3 message received",
- "from", req.FromUIN,
- "to", req.ToUIN,
- "type", fmt.Sprintf("0x%04X", req.MsgType),
- "msg_len", len(req.Message),
- )
- // 3. Call service layer with typed request
- ctx := context.Background()
- result, err := h.service.ProcessMessage(ctx, session, req)
- if err != nil {
- h.logger.Error("message processing failed",
- "from", req.FromUIN,
- "to", req.ToUIN,
- "err", err,
- )
- return nil
- }
- // 4. Route message based on service result
- if result.TargetOnline {
- // Target is online - forward message via dispatcher or direct send
- targetSession := h.sessions.GetSession(req.ToUIN)
- if targetSession != nil {
- if h.dispatcher != nil {
- // Use central dispatcher - routes to correct protocol based on target's version
- _ = h.dispatcher.SendOnlineMessage(targetSession, req.FromUIN, req.MsgType, req.Message)
- } else {
- // Fallback to V3 format if dispatcher not set
- _ = h.sendOnlineMessage(targetSession, req.FromUIN, req.MsgType, req.Message, seq2)
- }
- h.logger.Debug("V3 message forwarded",
- "from", req.FromUIN,
- "to", req.ToUIN,
- "target_version", targetSession.Version,
- )
- }
- } else if result.StoredOffline {
- // Message was stored for offline delivery by the service layer
- h.logger.Info("V3 message stored for offline delivery",
- "from", req.FromUIN,
- "to", req.ToUIN,
- "type", fmt.Sprintf("0x%04X", req.MsgType),
- )
- }
- return nil
- }
- // parseMessagePacket parses a V3 message packet into a typed MessageRequest struct.
- // Format: TIMESTAMP(4) + TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- func (h *V3Handler) parseMessagePacket(data []byte, fromUIN uint32) (MessageRequest, error) {
- req := MessageRequest{
- FromUIN: fromUIN,
- }
- if len(data) < 12 {
- return req, fmt.Errorf("message packet too short: %d bytes", len(data))
- }
- offset := 0
- // Skip timestamp (4 bytes)
- offset += 4
- // Target UIN (4 bytes)
- req.ToUIN = binary.LittleEndian.Uint32(data[offset : offset+4])
- offset += 4
- // Message type (2 bytes)
- req.MsgType = binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Message length (2 bytes)
- msgLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Message content
- if msgLen > 0 && offset+int(msgLen) <= len(data) {
- req.Message = string(data[offset : offset+int(msgLen)])
- // Remove null terminator if present
- if len(req.Message) > 0 && req.Message[len(req.Message)-1] == 0 {
- req.Message = req.Message[:len(req.Message)-1]
- }
- }
- return req, nil
- }
- // handleUserAdd processes user add to contact list (0x053C)
- // From iserverd v3_process_useradd(): adds user and sends "you were added" notification
- //
- // Refactored to use service layer (ProcessUserAdd) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> send notifications
- func (h *V3Handler) handleUserAdd(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to typed request struct
- req, err := h.parseUserAddPacket(data, uin)
- if err != nil {
- h.logger.Debug("V3 user add parse error", "uin", uin, "err", err)
- return nil
- }
- h.logger.Debug("V3 user add",
- "from", req.FromUIN,
- "target", req.TargetUIN,
- )
- // Add to contact list (handler responsibility - session management)
- contacts := session.GetContactList()
- contacts = append(contacts, req.TargetUIN)
- session.SetContactList(contacts)
- // 3. Call service layer with typed request
- ctx := context.Background()
- result, err := h.service.ProcessUserAdd(ctx, session.Instance, req)
- if err != nil {
- h.logger.Error("user add processing failed", "from", req.FromUIN, "target", req.TargetUIN, "err", err)
- return nil
- }
- // 4. Send notifications based on service result
- if result.TargetOnline {
- // Send target's online status to the user who added them
- _ = h.sendUserOnline(session, req.TargetUIN, result.TargetStatus)
- // Send "you were added" notification to target user if service says to
- if result.SendYouWereAdded {
- targetSession := h.sessions.GetSession(req.TargetUIN)
- if targetSession != nil {
- // Message format: nick#FE#first#FE#last#FE#email#FE#auth
- // Using UIN as nick for now since we don't have user info
- youWereAddedMsg := fmt.Sprintf("%d\xFE\xFE\xFE\xFE0", req.FromUIN)
- if h.dispatcher != nil {
- // Use dispatcher for cross-protocol support
- _ = h.dispatcher.SendOnlineMessage(targetSession, req.FromUIN, ICQLegacyMsgAdded, youWereAddedMsg)
- } else {
- // Fallback to V3 format
- _ = h.sendOnlineMessage(targetSession, req.FromUIN, ICQLegacyMsgAdded, youWereAddedMsg, 0)
- }
- // Also send the adder's online status to the target.
- // The target receives "you were added" but won't see the
- // adder as online unless we explicitly tell them.
- if h.dispatcher != nil {
- _ = h.dispatcher.SendUserOnline(targetSession, req.FromUIN, session.GetStatus())
- } else {
- _ = h.sendUserOnline(targetSession, req.FromUIN, session.GetStatus())
- }
- h.logger.Debug("V3 sent 'you were added' notification",
- "from", req.FromUIN,
- "to", req.TargetUIN,
- )
- }
- }
- }
- return nil
- }
- // parseUserAddPacket parses a V3 user add packet into a typed UserAddRequest struct.
- // Format: TIMESTAMP(4) + TARGET_UIN(4)
- func (h *V3Handler) parseUserAddPacket(data []byte, fromUIN uint32) (UserAddRequest, error) {
- req := UserAddRequest{
- FromUIN: fromUIN,
- }
- if len(data) < 8 {
- return req, fmt.Errorf("user add packet too short: %d bytes", len(data))
- }
- // Skip timestamp (4 bytes)
- // Target UIN (4 bytes)
- req.TargetUIN = binary.LittleEndian.Uint32(data[4:8])
- return req, nil
- }
- // handleGetInfo processes user info request (0x05FA)
- // From iserverd v3_process_getinfo(): sends basic, home, home_web, work, work_web info
- //
- // Refactored to use service layer (GetUserInfoForProtocol) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> build response
- func (h *V3Handler) handleGetInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to extract target UIN
- // Format: TIMESTAMP(4) + TARGET_UIN(4)
- if len(data) < 8 {
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[4:8])
- h.logger.Debug("V3 get info request",
- "from", uin,
- "target", targetUIN,
- )
- // 3. Call service layer to get user info
- ctx := context.Background()
- info, err := h.service.GetUserInfoForProtocol(ctx, targetUIN)
- if err != nil {
- h.logger.Error("failed to get user info", "target", targetUIN, "err", err)
- // Still send minimal info packets even on error
- info = &UserInfoResult{
- UIN: targetUIN,
- Nickname: fmt.Sprintf("%d", targetUIN),
- }
- }
- // 4. Build and send all user info packets using packet builder
- // (matching iserverd v3_process_getinfo which sends 5 packets)
- _ = h.sender.SendToSession(session, h.packetBuilder.BuildBasicInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
- _ = h.sender.SendToSession(session, h.packetBuilder.BuildHomeInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
- _ = h.sender.SendToSession(session, h.packetBuilder.BuildHomeWeb(session.NextServerSeqNum(), seq2, session.UIN, info))
- _ = h.sender.SendToSession(session, h.packetBuilder.BuildWorkInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
- _ = h.sender.SendToSession(session, h.packetBuilder.BuildWorkWeb(session.NextServerSeqNum(), seq2, session.UIN, info))
- return nil
- }
- // V3 packet sending helpers
- // sendAck sends ACK packet (V3 format from iserverd v3_send_ack)
- func (h *V3Handler) sendAck(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- // V3 ACK format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4)
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvAck)
- binary.LittleEndian.PutUint16(pkt[4:6], seq1)
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], uin)
- binary.LittleEndian.PutUint32(pkt[12:16], 0) // reserved
- return h.sender.SendPacket(addr, pkt)
- }
- // sendNotConnected sends not connected error
- func (h *V3Handler) sendNotConnected(addr *net.UDPAddr, seq2 uint16, uin uint32) error {
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvNotConnected)
- binary.LittleEndian.PutUint16(pkt[4:6], 0)
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], uin)
- binary.LittleEndian.PutUint32(pkt[12:16], 0)
- return h.sender.SendPacket(addr, pkt)
- }
- // sendUserOnline sends user online notification
- // From iserverd v3_send_user_online()
- func (h *V3Handler) sendUserOnline(session *LegacySession, uin uint32, status uint32) error {
- // V3 USER_ONLINE format from iserverd:
- // header(16) + UIN(4) + IP(4) + PORT(4) + INT_IP(4) + DC_TYPE(1) + STATUS(2) + ESTAT(2) + DCVER(2) + UNKNOWN(2)
- // Total: 16 + 4 + 4 + 4 + 4 + 1 + 2 + 2 + 2 + 2 = 41 bytes
- pkt := make([]byte, 41)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserOnline)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // UIN of the user who is online
- binary.LittleEndian.PutUint32(pkt[offset:], uin)
- offset += 4
- // IP address (0 for privacy - V3 clients crash on non-V3 client connect)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // TCP port (0 for privacy)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // Internal IP (0 for privacy)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // DC type (0)
- pkt[offset] = 0
- offset++
- // Status (low word)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status&0xFFFF))
- offset += 2
- // Extended status (high word)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status>>16))
- offset += 2
- // DC version
- binary.LittleEndian.PutUint16(pkt[offset:], 0)
- offset += 2
- // Unknown
- binary.LittleEndian.PutUint16(pkt[offset:], 0)
- offset += 2
- h.logger.Debug("sending V3 user online notification",
- "to", session.UIN,
- "online_uin", uin,
- "status", fmt.Sprintf("0x%08X", status),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // handleOfflineMsgReq processes offline message request (0x044C)
- // From iserverd v3_process_sysmsg_req()
- func (h *V3Handler) handleOfflineMsgReq(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- ctx := context.Background()
- // Fetch offline messages from database
- messages, err := h.service.GetOfflineMessages(ctx, uin)
- if err != nil {
- h.logger.Error("failed to get offline messages", "uin", uin, "err", err)
- return h.sendOfflineMsgDone(session, seq2)
- }
- h.logger.Debug("V3 offline message request",
- "uin", uin,
- "message_count", len(messages),
- )
- // Send each offline message
- for _, msg := range messages {
- if err := h.sendOfflineMessage(session, seq2, &msg); err != nil {
- h.logger.Error("failed to send offline message",
- "uin", uin,
- "from", msg.FromUIN,
- "err", err,
- )
- }
- }
- // Send end of offline messages
- return h.sendOfflineMsgDone(session, seq2)
- }
- // handleSetBasicInfo processes set basic info (0x050A)
- // Format: TIMESTAMP(4) + NICK_LEN(2)+NICK + FIRST_LEN(2)+FIRST + LAST_LEN(2)+LAST + EMAIL_LEN(2)+EMAIL
- func (h *V3Handler) handleSetBasicInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- if len(data) < 6 {
- return h.sendReplyOK(session, seq2, 0x01E0)
- }
- r := bytes.NewReader(data[4:]) // skip timestamp
- nick, _ := ParseLegacyString(r, true)
- first, _ := ParseLegacyString(r, true)
- last, _ := ParseLegacyString(r, true)
- email, _ := ParseLegacyString(r, true)
- ctx := context.Background()
- info := state.ICQBasicInfo{
- Nickname: nick,
- FirstName: first,
- LastName: last,
- EmailAddress: email,
- }
- if err := h.service.UpdateBasicInfo(ctx, uin, info); err != nil {
- h.logger.Error("V3 set basic info failed", "uin", uin, "err", err)
- }
- return h.sendReplyOK(session, seq2, 0x01E0)
- }
- // handleSetHomeInfo processes set home info (0x0582)
- // Format: TIMESTAMP(4) + ADDR_LEN(2)+ADDR + CITY_LEN(2)+CITY + STATE_LEN(2)+STATE + COUNTRY(2) + PHONE_LEN(2)+PHONE + FAX_LEN(2)+FAX + CELL_LEN(2)+CELL + ZIP(4) + GMT(2) + AUTH(1) + WEBAWARE(1) + HIDEIP(1) + PUBLISH_EMAIL(1)
- func (h *V3Handler) handleSetHomeInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- if len(data) < 6 {
- return h.sendReplyOK(session, seq2, 0x0280)
- }
- r := bytes.NewReader(data[4:]) // skip timestamp
- address, _ := ParseLegacyString(r, true)
- city, _ := ParseLegacyString(r, true)
- st, _ := ParseLegacyString(r, true)
- var country uint16
- _ = binary.Read(r, binary.LittleEndian, &country)
- phone, _ := ParseLegacyString(r, true)
- fax, _ := ParseLegacyString(r, true)
- cell, _ := ParseLegacyString(r, true)
- var zipCode uint32
- _ = binary.Read(r, binary.LittleEndian, &zipCode)
- var gmt uint16
- _ = binary.Read(r, binary.LittleEndian, &gmt)
- var auth, webaware uint8
- _ = binary.Read(r, binary.LittleEndian, &auth)
- _ = binary.Read(r, binary.LittleEndian, &webaware)
- // Read existing basic info to avoid overwriting nick/first/last/email
- ctx := context.Background()
- existing, err := h.service.GetFullUserInfo(ctx, uin)
- if err == nil && existing != nil {
- existing.ICQInfo.Basic.Address = address
- existing.ICQInfo.Basic.City = city
- existing.ICQInfo.Basic.State = st
- existing.ICQInfo.Basic.CountryCode = country
- existing.ICQInfo.Basic.Phone = phone
- existing.ICQInfo.Basic.Fax = fax
- existing.ICQInfo.Basic.CellPhone = cell
- existing.ICQInfo.Basic.ZIPCode = fmt.Sprintf("%d", zipCode)
- existing.ICQInfo.Basic.GMTOffset = uint8(gmt)
- if err := h.service.UpdateBasicInfo(ctx, uin, existing.ICQInfo.Basic); err != nil {
- h.logger.Error("V3 set home info failed", "uin", uin, "err", err)
- }
- }
- if err := h.service.SetAuthMode(ctx, uin, auth == 0); err != nil {
- h.logger.Error("V3 set home auth failed", "uin", uin, "err", err)
- }
- return h.sendReplyOK(session, seq2, 0x0280)
- }
- // handleSetHomeWeb processes set home web (0x058C)
- // Format: TIMESTAMP(4) + AGE(2) + SEX(1) + HP_LEN(2)+HP + BIRTH_YEAR(2) + BIRTH_MONTH(1) + BIRTH_DAY(1) + LANG1(1) + LANG2(1) + LANG3(1)
- func (h *V3Handler) handleSetHomeWeb(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- if len(data) < 7 {
- return h.sendReplyOK(session, seq2, 0x0294)
- }
- r := bytes.NewReader(data[4:]) // skip timestamp
- var age uint16
- _ = binary.Read(r, binary.LittleEndian, &age)
- var sex uint8
- _ = binary.Read(r, binary.LittleEndian, &sex)
- hp, _ := ParseLegacyString(r, true)
- var birthYear uint16
- _ = binary.Read(r, binary.LittleEndian, &birthYear)
- var birthMonth, birthDay, lang1, lang2, lang3 uint8
- _ = binary.Read(r, binary.LittleEndian, &birthMonth)
- _ = binary.Read(r, binary.LittleEndian, &birthDay)
- _ = binary.Read(r, binary.LittleEndian, &lang1)
- _ = binary.Read(r, binary.LittleEndian, &lang2)
- _ = binary.Read(r, binary.LittleEndian, &lang3)
- ctx := context.Background()
- info := state.ICQMoreInfo{
- Gender: uint16(sex),
- HomePageAddr: hp,
- BirthYear: birthYear,
- BirthMonth: birthMonth,
- BirthDay: birthDay,
- Lang1: lang1,
- Lang2: lang2,
- Lang3: lang3,
- }
- if err := h.service.UpdateMoreInfo(ctx, uin, info); err != nil {
- h.logger.Error("V3 set home web failed", "uin", uin, "err", err)
- }
- return h.sendReplyOK(session, seq2, 0x0294)
- }
- // handleSetWorkInfo processes set work info (0x0578)
- // Format: TIMESTAMP(4) + ADDR_LEN(2)+ADDR + CITY_LEN(2)+CITY + STATE_LEN(2)+STATE + COUNTRY(2) + COMPANY_LEN(2)+COMPANY + TITLE_LEN(2)+TITLE + DEPT(2) + PHONE_LEN(2)+PHONE + FAX_LEN(2)+FAX + PAGER_LEN(2)+PAGER + ZIP(4)
- func (h *V3Handler) handleSetWorkInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- if len(data) < 6 {
- return h.sendReplyOK(session, seq2, 0x026C)
- }
- r := bytes.NewReader(data[4:]) // skip timestamp
- address, _ := ParseLegacyString(r, true)
- city, _ := ParseLegacyString(r, true)
- st, _ := ParseLegacyString(r, true)
- var country uint16
- _ = binary.Read(r, binary.LittleEndian, &country)
- company, _ := ParseLegacyString(r, true)
- position, _ := ParseLegacyString(r, true)
- var dept uint16
- _ = binary.Read(r, binary.LittleEndian, &dept)
- phone, _ := ParseLegacyString(r, true)
- fax, _ := ParseLegacyString(r, true)
- // pager field — not stored in ICQWorkInfo, skip
- _, _ = ParseLegacyString(r, true)
- var zipCode uint32
- _ = binary.Read(r, binary.LittleEndian, &zipCode)
- // Read existing work info to preserve WebPage (set separately via 0x05BE)
- ctx := context.Background()
- existing, existErr := h.service.GetFullUserInfo(ctx, uin)
- if existErr == nil && existing != nil {
- existing.ICQInfo.Work.Address = address
- existing.ICQInfo.Work.City = city
- existing.ICQInfo.Work.State = st
- existing.ICQInfo.Work.CountryCode = country
- existing.ICQInfo.Work.Company = company
- existing.ICQInfo.Work.Position = position
- existing.ICQInfo.Work.Phone = phone
- existing.ICQInfo.Work.Fax = fax
- existing.ICQInfo.Work.ZIPCode = fmt.Sprintf("%d", zipCode)
- if err := h.service.UpdateWorkInfo(ctx, uin, existing.ICQInfo.Work); err != nil {
- h.logger.Error("V3 set work info failed", "uin", uin, "err", err)
- }
- }
- return h.sendReplyOK(session, seq2, 0x026C)
- }
- // handleSetWorkWeb processes set work web (0x05BE)
- // Format: TIMESTAMP(4) + WEB_LEN(2)+WEB
- func (h *V3Handler) handleSetWorkWeb(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- if len(data) < 6 {
- return h.sendReplyOK(session, seq2, 0x0258)
- }
- r := bytes.NewReader(data[4:]) // skip timestamp
- web, _ := ParseLegacyString(r, true)
- // Read existing work info to avoid overwriting other fields
- ctx := context.Background()
- existing, err := h.service.GetFullUserInfo(ctx, uin)
- if err == nil && existing != nil {
- existing.ICQInfo.Work.WebPage = web
- if err := h.service.UpdateWorkInfo(ctx, uin, existing.ICQInfo.Work); err != nil {
- h.logger.Error("V3 set work web failed", "uin", uin, "err", err)
- }
- }
- return h.sendReplyOK(session, seq2, 0x0258)
- }
- // handleVisibleList processes visible list (0x06AE)
- func (h *V3Handler) handleVisibleList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 visible list", "uin", uin)
- return nil
- }
- // handleInvisibleList processes invisible list (0x06A4)
- func (h *V3Handler) handleInvisibleList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 invisible list", "uin", uin)
- return nil
- }
- // handleUnknownDep processes unknown pre-auth related request (0x0604)
- func (h *V3Handler) handleUnknownDep(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 unknown dep request", "uin", uin)
- return nil
- }
- // handleSearchStart processes search user request (0x05C8)
- // From V3 protocol: TIMESTAMP(4) + TYPE(2) + COMP(2) + LEN(2) + STRING
- // Type: 0x00FF=UIN, 0x01FF=Nick, 0x02FF=First, 0x03FF=Last, etc.
- // Comp: 0x0000=contains, 0x0001=doesn't contain, 0x0002=is, etc.
- func (h *V3Handler) handleSearchStart(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- ctx := context.Background()
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse search parameters - format: TIMESTAMP(4) + TYPE(2) + COMP(2) + LEN(2) + STRING
- if len(data) < 10 {
- h.logger.Debug("V3 search start - packet too short", "uin", uin, "len", len(data))
- return h.sendSearchDone(session, seq2)
- }
- offset := 4 // Skip timestamp
- searchType := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- compType := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- strLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- var searchStr string
- if len(data) >= offset+int(strLen) && strLen > 0 {
- searchStr = string(data[offset : offset+int(strLen)])
- // Remove null terminator if present
- if len(searchStr) > 0 && searchStr[len(searchStr)-1] == 0 {
- searchStr = searchStr[:len(searchStr)-1]
- }
- }
- h.logger.Debug("V3 search start",
- "uin", uin,
- "search_type", fmt.Sprintf("0x%04X", searchType),
- "comp_type", compType,
- "search_str", searchStr,
- )
- // Handle UIN search (type 0x00FF)
- if searchType == 0x00FF && searchStr != "" {
- // Try to parse as UIN
- var targetUIN uint64
- _, err := fmt.Sscanf(searchStr, "%d", &targetUIN)
- if err == nil && targetUIN > 0 {
- // Search in database first (includes both online and offline users)
- result, err := h.service.SearchByUIN(ctx, uint32(targetUIN))
- if err == nil && result != nil {
- // User found in database - truncate fields for legacy client safety
- nick := truncateField(result.Nickname, 20, h.logger, "nickname", result.UIN)
- first := truncateField(result.FirstName, 64, h.logger, "first_name", result.UIN)
- last := truncateField(result.LastName, 64, h.logger, "last_name", result.UIN)
- email := truncateField(result.Email, 64, h.logger, "email", result.UIN)
- _ = h.sendSearchFound(session, seq2, result.UIN, nick, first, last, email, 0)
- h.logger.Debug("V3 search found user in database",
- "searcher", uin,
- "found", result.UIN,
- "nick", result.Nickname,
- )
- } else {
- h.logger.Debug("V3 search - user not found",
- "searcher", uin,
- "target", targetUIN,
- "err", err,
- )
- }
- }
- }
- // Send search done
- return h.sendSearchDone(session, seq2)
- }
- // handleGetDeps1 processes in-session pre-auth list request (0x05F0).
- // Historically called "get departments list" in iserverd. Used by the client
- // to replicate the pre-auth response during a normal session.
- // From iserverd v3_process_getdeps1() - sends ICQ_CMDxSND_USERxDEPS_LIST1 (0x0082)
- func (h *V3Handler) handleGetDeps1(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 get deps1 request", "uin", uin)
- // Send deps list using 0x0082 (during session, not login)
- return h.sendDeptsList1(session, seq2)
- }
- // sendDeptsList1 sends the in-session pre-auth list response (0x0082).
- // Historically called "departments list" in iserverd (from its Users_Deps database table).
- // Different from login depslist (0x0032).
- func (h *V3Handler) sendDeptsList1(session *LegacySession, seq2 uint16) error {
- // V3 DEPS_LIST1 format from iserverd:
- // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) +
- // UNKNOWN(2) + DEPLIST_VERSION(4) + COUNT(4) + [deps...] + TRAILER(4)
- // For now, send empty list with version 1
- pkt := make([]byte, 32)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserDepsList1) // 0x0082 - ICQ_CMDxSND_USERxDEPS_LIST1
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // Unknown field (0x003C from iserverd)
- binary.LittleEndian.PutUint16(pkt[offset:], 0x003C)
- offset += 2
- // Deplist version
- binary.LittleEndian.PutUint32(pkt[offset:], 1)
- offset += 4
- // Count = 0 (empty deps list)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // Trailer from iserverd
- binary.LittleEndian.PutUint16(pkt[offset:], 0x0002)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0x002a)
- offset += 2
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendOfflineMessage sends an offline message to a V3 client
- // From iserverd v3_send_offline_message()
- // Format: header(16) + FROM_UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MINUTE(1) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- func (h *V3Handler) sendOfflineMessage(session *LegacySession, seq2 uint16, msg *LegacyOfflineMessage) error {
- // Build message data
- msgBytes := []byte(msg.Message)
- pktLen := 16 + 4 + 2 + 1 + 1 + 1 + 1 + 2 + 2 + len(msgBytes) + 1
- pkt := make([]byte, pktLen)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvSysMsgOffline) // 0x00DC
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // From UIN
- binary.LittleEndian.PutUint32(pkt[offset:], msg.FromUIN)
- offset += 4
- // Timestamp: YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MINUTE(1)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(msg.Timestamp.Year()))
- offset += 2
- pkt[offset] = byte(msg.Timestamp.Month())
- offset++
- pkt[offset] = byte(msg.Timestamp.Day())
- offset++
- pkt[offset] = byte(msg.Timestamp.Hour())
- offset++
- pkt[offset] = byte(msg.Timestamp.Minute())
- offset++
- // Message type
- binary.LittleEndian.PutUint16(pkt[offset:], msg.MsgType)
- offset += 2
- // Message length (including null terminator)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(msgBytes)+1))
- offset += 2
- // Message content
- copy(pkt[offset:], msgBytes)
- offset += len(msgBytes)
- pkt[offset] = 0 // null terminator
- h.logger.Debug("sending V3 offline message",
- "to", session.UIN,
- "from", msg.FromUIN,
- "type", fmt.Sprintf("0x%04X", msg.MsgType),
- "timestamp", msg.Timestamp,
- )
- return h.sender.SendToSession(session, pkt)
- }
- // sendOfflineMsgDone sends end of offline messages
- func (h *V3Handler) sendOfflineMsgDone(session *LegacySession, seq2 uint16) error {
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], 0x00E6) // ICQ_CMDxSND_SYSxMSGxDONE
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- binary.LittleEndian.PutUint32(pkt[12:16], 0)
- return h.sender.SendToSession(session, pkt)
- }
- // sendReplyOK sends a generic OK response
- func (h *V3Handler) sendReplyOK(session *LegacySession, seq2 uint16, command uint16) error {
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], command)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- binary.LittleEndian.PutUint32(pkt[12:16], 0)
- return h.sender.SendToSession(session, pkt)
- }
- // sendSearchDone sends search complete response
- // From iserverd v3_send_search_finished()
- // Packet format (17 bytes total):
- //
- // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) + MORE(1)
- //
- // The 'more' parameter indicates if there are more search results available:
- //
- // 0 = no more results, 1 = more results available (pagination)
- func (h *V3Handler) sendSearchDone(session *LegacySession, seq2 uint16, more ...bool) error {
- pkt := make([]byte, 17)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], 0x00A0) // ICQ_CMDxSND_SEARCHxDONE
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- binary.LittleEndian.PutUint32(pkt[12:16], 0) // reserved
- // Set 'more' flag - indicates if more search results are available
- if len(more) > 0 && more[0] {
- pkt[16] = 1 // more results available
- } else {
- pkt[16] = 0 // no more results
- }
- return h.sender.SendToSession(session, pkt)
- }
- // sendSearchFound sends a search result
- // From iserverd v3_send_found_info()
- // Format: header(16) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST +
- //
- // LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
- func (h *V3Handler) sendSearchFound(session *LegacySession, seq2 uint16, foundUIN uint32, nick, first, last, email string, auth uint8) error {
- // Calculate packet size
- pktSize := 16 + 4 + 2 + len(nick) + 1 + 2 + len(first) + 1 + 2 + len(last) + 1 + 2 + len(email) + 1 + 1
- pkt := make([]byte, pktSize)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0x008C) // ICQ_CMDxSND_SEARCHxFOUND
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // Found user UIN
- binary.LittleEndian.PutUint32(pkt[offset:], foundUIN)
- offset += 4
- // Nick (length-prefixed with null terminator)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(nick)+1))
- offset += 2
- copy(pkt[offset:], nick)
- offset += len(nick)
- pkt[offset] = 0
- offset++
- // First name
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(first)+1))
- offset += 2
- copy(pkt[offset:], first)
- offset += len(first)
- pkt[offset] = 0
- offset++
- // Last name
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(last)+1))
- offset += 2
- copy(pkt[offset:], last)
- offset += len(last)
- pkt[offset] = 0
- offset++
- // Email
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(email)+1))
- offset += 2
- copy(pkt[offset:], email)
- offset += len(email)
- pkt[offset] = 0
- offset++
- // Auth flag
- pkt[offset] = auth
- offset++
- h.logger.Debug("V3 sending search found",
- "to", session.UIN,
- "found_uin", foundUIN,
- "nick", nick,
- "packet_hex", fmt.Sprintf("%X", pkt[:offset]),
- "packet_len", offset,
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendOnlineMessage sends an online system message to a V3/V4 client
- // From iserverd v3_send_user_message()
- // Format: header(16) + FROM_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- // Note: V3 packets do NOT have a checkcode - they have a reserved field (always 0)
- func (h *V3Handler) sendOnlineMessage(session *LegacySession, fromUIN uint32, msgType uint16, message string, seq2 uint16) error {
- msgBytes := []byte(message)
- h.logger.Debug("V3 sending online message",
- "to", session.UIN,
- "from", fromUIN,
- "type", fmt.Sprintf("0x%04X", msgType),
- "msg_len", len(msgBytes),
- )
- // Send V3 format packet
- pktSize := 16 + 4 + 2 + 2 + len(msgBytes) + 1
- pkt := make([]byte, pktSize)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvSysMsgOnline) // 0x0104
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved (V3 has NO checkcode!)
- offset += 4
- // From UIN
- binary.LittleEndian.PutUint32(pkt[offset:], fromUIN)
- offset += 4
- // Message type (mask high byte for V3 clients)
- binary.LittleEndian.PutUint16(pkt[offset:], msgType&0x00FF)
- offset += 2
- // Message length (including null terminator)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(msgBytes)+1))
- offset += 2
- // Message content
- copy(pkt[offset:], msgBytes)
- offset += len(msgBytes)
- pkt[offset] = 0 // null terminator
- offset++
- h.logger.Debug("V3 sending online message packet",
- "to", session.UIN,
- "from", fromUIN,
- "packet_hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendUserOffline sends user offline notification
- // From iserverd v3_send_user_offline()
- func (h *V3Handler) sendUserOffline(session *LegacySession, uin uint32) error {
- // V3 USER_OFFLINE format: header(16) + UIN(4)
- pkt := make([]byte, 20)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserOffline)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // UIN of the user who went offline
- binary.LittleEndian.PutUint32(pkt[offset:], uin)
- offset += 4
- h.logger.Debug("sending V3 user offline notification",
- "to", session.UIN,
- "offline_uin", uin,
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendUserStatus sends user status change notification
- // From iserverd v3_send_user_status() in make_packet.cpp
- // This is used when a user changes status while already online
- // (e.g., Away -> Online, Online -> DND)
- // Format: header(16) + UIN(4) + STATUS(2) + ESTAT(2)
- func (h *V3Handler) sendUserStatus(session *LegacySession, uin uint32, status uint32) error {
- // V3 USER_STATUS format from iserverd:
- // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) +
- // TARGET_UIN(4) + STATUS(2) + ESTAT(2)
- pkt := make([]byte, 24)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserStatus) // 0x01A4
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
- offset += 4
- // UIN of the user whose status changed
- binary.LittleEndian.PutUint32(pkt[offset:], uin)
- offset += 4
- // Status (low word)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status&0xFFFF))
- offset += 2
- // Extended status (high word)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status>>16))
- offset += 2
- h.logger.Debug("sending V3 user status change notification",
- "to", session.UIN,
- "changed_uin", uin,
- "status", fmt.Sprintf("0x%08X", status),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // handleGetNotes processes get notes request (0x05AA)
- // From iserverd v3_process_notes() - returns user's notes
- func (h *V3Handler) handleGetNotes(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse: TIMESTAMP(4) + TARGET_UIN(4)
- if len(data) < 8 {
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[4:8])
- h.logger.Debug("V3 get notes", "uin", uin, "target", targetUIN)
- // Retrieve notes from database via service layer
- ctx := context.Background()
- notes, err := h.service.GetNotes(ctx, targetUIN)
- if err != nil {
- h.logger.Debug("V3 get notes failed", "uin", uin, "target", targetUIN, "err", err)
- // Send empty notes on error
- notes = ""
- }
- // Send notes response
- return h.sendNotes(session, seq2, targetUIN, notes)
- }
- // handleSetNotes processes set notes request (0x0596)
- // From iserverd v3_process_setnotes()
- // Data format: TIMESTAMP(4) + NOTES_LEN(2) + NOTES
- func (h *V3Handler) handleSetNotes(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse notes from packet
- // Format: TIMESTAMP(4) + NOTES_LEN(2) + NOTES
- if len(data) < 6 {
- h.logger.Debug("V3 set notes - data too short", "uin", uin, "len", len(data))
- return h.sendReplyOK(session, seq2, 0x01CC)
- }
- offset := 4 // Skip timestamp
- // Read notes length (2 bytes)
- notesLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Read notes content
- var notes string
- if notesLen > 0 && offset+int(notesLen) <= len(data) {
- notes = string(data[offset : offset+int(notesLen)])
- // Remove null terminator if present
- if len(notes) > 0 && notes[len(notes)-1] == 0 {
- notes = notes[:len(notes)-1]
- }
- }
- h.logger.Debug("V3 set notes", "uin", uin, "notes_len", len(notes))
- // Save notes to database via service layer
- ctx := context.Background()
- if err := h.service.SetNotes(ctx, uin, notes); err != nil {
- h.logger.Error("V3 set notes failed", "uin", uin, "err", err)
- // Still send OK response to client (matching iserverd behavior)
- }
- // Send OK response (0x01CC = ICQ_CMDxSND_USERxSET_NOTES_OK)
- return h.sendReplyOK(session, seq2, 0x01CC)
- }
- // handleSetPassword processes set password request (0x049C)
- // From iserverd v3_process_setpass()
- // Data format: TIMESTAMP(4) + PASSWORD_LEN(2) + NEW_PASSWORD
- // Note: iserverd doesn't validate old password, just updates to new password
- func (h *V3Handler) handleSetPassword(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse password from data
- // Format: TIMESTAMP(4) + PASSWORD_LEN(2) + NEW_PASSWORD
- if len(data) < 6 {
- h.logger.Debug("V3 set password - data too short", "uin", uin, "len", len(data))
- return h.sendReplyOK(session, seq2, 0x0140)
- }
- offset := 0
- // Skip timestamp (4 bytes)
- offset += 4
- // Read password length (2 bytes)
- pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Sanity check password length
- if pwdLen > 32 {
- pwdLen = 32
- }
- // Read new password
- if offset+int(pwdLen) > len(data) {
- h.logger.Debug("V3 set password - password truncated", "uin", uin)
- return h.sendReplyOK(session, seq2, 0x0140)
- }
- newPassword := string(data[offset : offset+int(pwdLen)])
- // Remove null terminator if present
- if len(newPassword) > 0 && newPassword[len(newPassword)-1] == 0 {
- newPassword = newPassword[:len(newPassword)-1]
- }
- h.logger.Info("V3 set password request",
- "uin", uin,
- "new_password_len", len(newPassword),
- )
- // Update password using service
- // Note: iserverd doesn't validate old password, so we pass empty string
- ctx := context.Background()
- if err := h.service.SetPassword(ctx, uin, "", newPassword); err != nil {
- h.logger.Error("V3 set password failed",
- "uin", uin,
- "err", err,
- )
- // Still send OK response to match iserverd behavior
- // (iserverd doesn't have error handling for this)
- } else {
- h.logger.Info("V3 password updated successfully", "uin", uin)
- }
- // Send OK response (0x0140 = ICQ_CMDxSND_USERxSET_PASSWD_OK)
- return h.sendReplyOK(session, seq2, 0x0140)
- }
- // handleSetAuth processes set auth mode request (0x0514)
- // From iserverd v3_process_setauth()
- // Data format: TIMESTAMP(4) + AUTH_MODE(1)
- // - AUTH_MODE: 0 = no auth required, 1 = auth required
- func (h *V3Handler) handleSetAuth(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse auth mode - format: TIMESTAMP(4) + AUTH_MODE(1)
- // From iserverd v3_process_setauth():
- // int_pack >> ... >> temp_stamp;
- // auth_mode = 0;
- // int_pack >> auth_mode;
- authRequired := false
- if len(data) >= 5 {
- // Skip timestamp (4 bytes), read auth_mode (1 byte)
- authMode := data[4]
- authRequired = authMode != 0
- }
- h.logger.Debug("V3 set auth",
- "uin", uin,
- "auth_required", authRequired,
- )
- // Update auth mode in database
- ctx := context.Background()
- if err := h.service.SetAuthMode(ctx, uin, authRequired); err != nil {
- h.logger.Error("V3 set auth failed",
- "uin", uin,
- "err", err,
- )
- // Still send OK response to match iserverd behavior
- }
- // Send OK response (0x01F4 = ICQ_CMDxSND_USERxSET_AUTH_OK)
- return h.sendReplyOK(session, seq2, 0x01F4)
- }
- // handleSetState processes set client state request (0x0528)
- // From iserverd v3_process_state() - client state/mode changes
- // This is the V3 status change command - when a user changes their status
- // (e.g., Online -> Away, Away -> DND), this handler updates the session
- // and broadcasts the status change to all contacts.
- //
- // Data format: TIMESTAMP(4) + STATUS(4) + ESTATUS(4)
- // - STATUS: Low 16 bits of status (Online=0, Away=1, DND=2, NA=4, etc.)
- // - ESTATUS: High 16 bits of status (extended status flags)
- func (h *V3Handler) handleSetState(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse status - format: TIMESTAMP(4) + STATUS(4) + ESTATUS(4)
- // From iserverd v3_process_status():
- // int_pack >> ... >> new_status >> new_estatus;
- if len(data) < 12 {
- h.logger.Debug("V3 set state - data too short", "uin", uin, "len", len(data))
- return nil
- }
- // Skip timestamp (4 bytes)
- offset := 4
- // Read status (4 bytes) - low word is status, high word is extended status
- newStatus := binary.LittleEndian.Uint32(data[offset : offset+4])
- offset += 4
- // Read extended status (4 bytes) - some clients send this separately
- newEStatus := binary.LittleEndian.Uint32(data[offset : offset+4])
- // Combine status and extended status (iserverd: user.status = new_status, user.estat = new_estatus)
- // The full status is stored as: status | (estatus << 16)
- fullStatus := newStatus | (newEStatus << 16)
- oldStatus := session.GetStatus()
- session.SetStatus(fullStatus)
- h.logger.Info("V3 status change",
- "uin", uin,
- "old_status", fmt.Sprintf("0x%08X", oldStatus),
- "new_status", fmt.Sprintf("0x%08X", fullStatus),
- )
- // Broadcast status change to all contacts who have this user in their list
- // From iserverd v3_process_status(): broadcast_status(user, old_status)
- h.broadcastStatusChange(session, fullStatus)
- return nil
- }
- // broadcastStatusChange notifies all contacts who have this user in their list
- // that the user's status has changed.
- // From iserverd broadcast_status() in broadcast.cpp - this function finds all
- // online users who have the target user in their contact list and sends them
- // a status update notification.
- func (h *V3Handler) broadcastStatusChange(session *LegacySession, newStatus uint32) {
- if h.dispatcher == nil {
- return
- }
- // Find all sessions that have this user in their contact list
- // This is the same logic used for online/offline notifications
- contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
- h.logger.Debug("broadcasting status change to contacts",
- "uin", session.UIN,
- "new_status", fmt.Sprintf("0x%08X", newStatus),
- "contacts_to_notify", contactsToNotify,
- )
- for _, contactUIN := range contactsToNotify {
- contactSession := h.sessions.GetSession(contactUIN)
- if contactSession != nil {
- // Use dispatcher to send status change in correct protocol format
- // This routes to V3 or V5 handler based on the contact's protocol version
- _ = h.dispatcher.SendStatusChange(contactSession, session.UIN, newStatus)
- }
- }
- }
- // handleUsageStats processes usage statistics (0x0532)
- // From iserverd v3_process_usagestats() - client sends usage data
- func (h *V3Handler) handleUsageStats(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 usage stats", "uin", uin)
- return nil
- }
- // handleGetExternals processes get externals request (0x04C4)
- // From iserverd v3_process_getext() - returns external services list
- func (h *V3Handler) handleGetExternals(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 get externals", "uin", uin)
- // Send externals response (empty list)
- return h.sendExternals(session, seq2)
- }
- // handleSysAck processes system message acknowledgment (0x0442)
- // From iserverd v3_process_sysack() - client acknowledges receipt of offline messages
- // After receiving this, we delete the offline messages from the database
- func (h *V3Handler) handleSysAck(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- ctx := context.Background()
- // Delete offline messages for this user
- if err := h.service.AckOfflineMessages(ctx, uin); err != nil {
- h.logger.Error("failed to delete offline messages", "uin", uin, "err", err)
- } else {
- h.logger.Debug("V3 offline messages deleted", "uin", uin)
- }
- return nil
- }
- // handleOnlineInfo processes reconnect/online info request (0x015E)
- // From iserverd v3_process_onlineinfo()
- func (h *V3Handler) handleOnlineInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V3 online info", "uin", uin)
- return nil
- }
- // handleGetInfo1 processes get basic info only request (0x0460)
- // From iserverd v3_process_getinfo1() - sends only basic info (not full 5-packet set)
- //
- // Refactored to use service layer (GetUserInfoForProtocol) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> build response
- func (h *V3Handler) handleGetInfo1(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to extract target UIN
- // Format: TIMESTAMP(4) + TARGET_UIN(4)
- if len(data) < 8 {
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[4:8])
- h.logger.Debug("V3 get info1", "uin", uin, "target", targetUIN)
- // 3. Call service layer to get user info
- ctx := context.Background()
- info, err := h.service.GetUserInfoForProtocol(ctx, targetUIN)
- if err != nil {
- h.logger.Error("failed to get user info", "target", targetUIN, "err", err)
- // Still send minimal info packet even on error
- info = &UserInfoResult{
- UIN: targetUIN,
- Nickname: fmt.Sprintf("%d", targetUIN),
- }
- }
- // 4. Build and send only basic info using packet builder
- // (unlike handleGetInfo which sends all 5 packets)
- return h.sender.SendToSession(session, h.packetBuilder.BuildBasicInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
- }
- // sendNotes sends notes response (0x0352)
- // From iserverd v3_send_notes()
- func (h *V3Handler) sendNotes(session *LegacySession, seq2 uint16, targetUIN uint32, notes string) error {
- pktSize := 16 + 2 + len(notes) + 1 + 12 // header + notes + timestamps
- pkt := make([]byte, pktSize)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0x0352) // ICQ_CMDxSND_USERxNOTES
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // Notes (length-prefixed)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(notes)+1))
- offset += 2
- copy(pkt[offset:], notes)
- offset += len(notes)
- pkt[offset] = 0
- offset++
- // Timestamps: nupdate(4) + lastlogin(4) + ip_addr(4)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendExternals sends externals list response (0x00B4)
- // From iserverd v3_send_externals()
- func (h *V3Handler) sendExternals(session *LegacySession, seq2 uint16) error {
- pkt := make([]byte, 20)
- offset := 0
- // Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0x00B4) // ICQ_CMDxSND_USERxEXTERNALS
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // Count = 0 (no externals)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- return h.sender.SendToSession(session, pkt[:offset])
- }
|