| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372 |
- package icq_legacy
- import (
- "bytes"
- "context"
- "encoding/binary"
- "fmt"
- "log/slog"
- "net"
- "strconv"
- "github.com/mk6i/open-oscar-server/state"
- )
- // V4Handler handles ICQ V4 protocol packets
- // V4 packet format (from wumpus documentation):
- // VERSION(2) + RANDOM(2) + ZEROS(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA...
- // Total header: 20 bytes
- // V4 uses XOR encryption similar to V3 but with different positions
- type V4Handler struct {
- sessions *LegacySessionManager
- service LegacyService
- sender PacketSender
- packetBuilder V4PacketBuilder // Packet builder for constructing V4 protocol packets
- dispatcher MessageDispatcher // Central dispatcher for cross-protocol messaging
- logger *slog.Logger
- }
- // NewV4Handler creates a new V4 protocol handler
- func NewV4Handler(
- sessions *LegacySessionManager,
- service LegacyService,
- sender PacketSender,
- packetBuilder V4PacketBuilder,
- logger *slog.Logger,
- ) *V4Handler {
- return &V4Handler{
- 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 *V4Handler) SetSender(sender PacketSender) {
- h.sender = sender
- }
- // SetDispatcher sets the message dispatcher for cross-protocol messaging
- func (h *V4Handler) SetDispatcher(dispatcher MessageDispatcher) {
- h.dispatcher = dispatcher
- }
- // V4 packet offsets (from wumpus documentation)
- const (
- v4OffsetVersion = 0
- v4OffsetRandom = 2
- v4OffsetZero = 4
- v4OffsetCommand = 6
- v4OffsetSeq1 = 8
- v4OffsetSeq2 = 10
- v4OffsetUIN = 12
- v4OffsetCheckcode = 16
- v4HeaderSize = 20
- )
- // Handle processes a V4 protocol packet
- func (h *V4Handler) Handle(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
- if len(packet) < v4HeaderSize {
- return fmt.Errorf("V4 packet too short: %d bytes", len(packet))
- }
- h.logger.Debug("raw V4 packet before decryption",
- "hex", fmt.Sprintf("%X", packet),
- "len", len(packet),
- )
- // Make a copy for decryption
- decrypted := make([]byte, len(packet))
- copy(decrypted, packet)
- // Decrypt the V4 packet
- if err := h.decryptV4Packet(decrypted); err != nil {
- h.logger.Debug("V4 decryption failed", "err", err)
- return err
- }
- h.logger.Debug("V4 packet after decryption",
- "hex", fmt.Sprintf("%X", decrypted),
- )
- // Parse V4 header
- command := binary.LittleEndian.Uint16(decrypted[v4OffsetCommand:])
- seq1 := binary.LittleEndian.Uint16(decrypted[v4OffsetSeq1:])
- seq2 := binary.LittleEndian.Uint16(decrypted[v4OffsetSeq2:])
- uin := binary.LittleEndian.Uint32(decrypted[v4OffsetUIN:])
- // Extract data after header
- var data []byte
- if len(decrypted) > v4HeaderSize {
- data = decrypted[v4HeaderSize:]
- }
- h.logger.Info("V4 packet received",
- "command", fmt.Sprintf("0x%04X", command),
- "uin", uin,
- "seq1", seq1,
- "seq2", seq2,
- "data_len", len(data),
- "addr", addr.String(),
- "session_found", session != nil,
- )
- // 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.
- // V4 keep-alive already does a UIN-based session lookup fallback,
- // but all other commands silently return nil. This catches them.
- if session == nil {
- switch command {
- case ICQLegacyCmdAck, ICQLegacyCmdFirstLogin,
- ICQLegacyCmdGetDeps, ICQLegacyCmdLogin,
- ICQLegacyCmdRegRequestInfo, ICQLegacyCmdRegNewUserInfo:
- // Allow these through - they don't require a session
- default:
- h.logger.Info("V4 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 V4 commands (same commands as V3, just different packet format)
- switch command {
- case ICQLegacyCmdFirstLogin:
- return h.handleFirstLogin(addr, seq1, seq2, uin)
- case ICQLegacyCmdRegRequestInfo:
- return h.handleRegRequestInfo(addr, seq1, seq2, uin)
- case ICQLegacyCmdRegNewUserInfo:
- return h.handleRegNewUserInfo(addr, seq1, seq2, uin, data)
- case ICQLegacyCmdLogin:
- return h.handleLogin(session, addr, seq1, seq2, uin, data)
- case ICQLegacyCmdGetDeps:
- return h.handleGetDeps(addr, seq1, seq2, uin, data)
- case ICQLegacyCmdAck:
- h.logger.Debug("received V4 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 ICQLegacyCmdUserGetInfo:
- return h.handleGetInfo(session, seq1, seq2, uin, data)
- case ICQLegacyCmdInfoReq:
- return h.handleInfoReq(session, seq1, seq2, uin, data)
- case ICQLegacyCmdExtInfoReq:
- return h.handleExtInfoReq(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSetBasicInfo: // 0x050A - V4-specific update basic info
- return h.handleUpdateBasic(session, seq1, seq2, uin, data)
- case ICQLegacyCmdUpdateDetail: // 0x04B0 - update extended info
- return h.handleUpdateDetail(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSearchStart: // 0x05C8 - not yet implemented
- h.logger.Debug("V4 search not yet implemented", "uin", uin)
- return h.sendAck(addr, seq1, seq2, uin)
- case ICQLegacyCmdSearchUIN: // 0x041A - search by UIN (old)
- return h.handleSearchByUIN(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSearchUser: // 0x0424 - search by name/email (old)
- return h.handleSearchByName(session, seq1, seq2, uin, data)
- case ICQLegacyCmdSysMsgDoneAck: // 0x0442 - offline messages acknowledged
- h.logger.Debug("V4 offline msg done ack", "uin", uin)
- return nil
- case ICQLegacyCmdVisibleList: // 0x06AE - visible list
- h.logger.Debug("V4 visible list received (stub)", "uin", uin)
- return h.sendAck(addr, seq1, seq2, uin)
- case ICQLegacyCmdInvisibleList: // 0x06A4 - invisible list
- h.logger.Debug("V4 invisible list received (stub)", "uin", uin)
- return h.sendAck(addr, seq1, seq2, uin)
- case ICQLegacyCmdMetaUser: // 0x064A - META commands (V4+)
- h.logger.Debug("V4 META command not yet implemented", "uin", uin)
- return h.sendAck(addr, seq1, seq2, uin)
- default:
- h.logger.Debug("unhandled V4 command",
- "command", fmt.Sprintf("0x%04X", command),
- "uin", uin,
- )
- // Send ACK for unknown commands
- return h.sendAck(addr, seq1, seq2, uin)
- }
- }
- // decryptV4Packet decrypts a V4 packet in place
- // From wumpus code in v4-notes.txt:
- // count = ((length + 3) / 4 + 3) / 4
- // This gives the number of DWORDs to encrypt
- // Skip DWORD 4 (checkcode at offset 16-19)
- func (h *V4Handler) decryptV4Packet(packet []byte) error {
- if len(packet) < v4HeaderSize {
- return fmt.Errorf("packet too short for V4 decryption")
- }
- // Get checkcode from position 16-19 (little-endian)
- checkcode := binary.LittleEndian.Uint32(packet[v4OffsetCheckcode:])
- // Calculate encryption key
- // key = packetLen * 0x66756B65 + checkcode
- packetLen := len(packet)
- key := uint32(packetLen)*0x66756B65 + checkcode
- // Calculate number of DWORDs to decrypt using wumpus formula:
- // count = ((length + 3) / 4 + 3) / 4
- count := ((packetLen+3)/4 + 3) / 4
- // Decrypt each DWORD, but skip DWORD 4 (checkcode at offset 16)
- for i := 0; i < count; i++ {
- pos := i * 4
- // Skip checkcode DWORD (position 16-19, i.e., DWORD 4)
- if i == 4 {
- continue
- }
- // Stop if we've gone past the packet
- if pos >= packetLen {
- break
- }
- tableIdx := pos & 0xFF
- xorVal := key + uint32(V4Table[tableIdx])
- // XOR up to 4 bytes
- for j := 0; j < 4 && pos+j < packetLen; j++ {
- packet[pos+j] ^= byte(xorVal >> (j * 8))
- }
- }
- // Restore version bytes (they should be 04 00)
- packet[0] = 0x04
- packet[1] = 0x00
- return nil
- }
- // handleFirstLogin processes the first login packet (0x04EC)
- // This is sent by clients starting the registration wizard
- // Server should respond with registration info (admin notes)
- func (h *V4Handler) handleFirstLogin(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- h.logger.Info("V4 first login packet - registration request",
- "uin", uin,
- "seq1", seq1,
- "seq2", seq2,
- "addr", addr.String(),
- )
- // Send ACK first
- _ = h.sendAck(addr, seq1, seq2, uin)
- // Send registration info with admin notes
- // This tells the client that registration is enabled and provides any admin notes
- return h.sendRegisterInfo(addr, seq2, uin)
- }
- // handleRegRequestInfo processes the registration info request (0x05DC)
- // Client is asking for admin notes and registration status
- func (h *V4Handler) handleRegRequestInfo(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- h.logger.Info("V4 registration info request",
- "uin", uin,
- "seq1", seq1,
- "seq2", seq2,
- "addr", addr.String(),
- )
- // Send ACK first
- _ = h.sendAck(addr, seq1, seq2, uin)
- // Send registration info
- return h.sendRegisterInfo(addr, seq2, uin)
- }
- // handleRegNewUserInfo processes the registration form submission (0x05E6)
- // Client is submitting their registration data
- func (h *V4Handler) handleRegNewUserInfo(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32, data []byte) error {
- ctx := context.Background()
- h.logger.Info("V4 registration form received",
- "uin", uin,
- "seq1", seq1,
- "seq2", seq2,
- "data_len", len(data),
- "addr", addr.String(),
- )
- // Send ACK first
- _ = h.sendAck(addr, seq1, seq2, uin)
- // Parse registration data
- // Format from client.html (0x05e6):
- // CLIENT_VERSION(2) + NICK_LEN(2) + NICK + FNAME_LEN(2) + FNAME + LNAME_LEN(2) + LNAME + EMAIL_LEN(2) + EMAIL + ...
- if len(data) < 10 {
- h.logger.Debug("V4 registration data too short", "len", len(data))
- return h.sendNotConnected(addr, seq2, uin)
- }
- offset := 0
- // Read client version (2 bytes)
- // clientVersion := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- // Read nickname
- if offset+2 > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- nickLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- if nickLen > 32 || offset+int(nickLen) > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- nickname := string(data[offset : offset+int(nickLen)])
- if len(nickname) > 0 && nickname[len(nickname)-1] == 0 {
- nickname = nickname[:len(nickname)-1]
- }
- offset += int(nickLen)
- // Read first name
- if offset+2 > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- fnameLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- if fnameLen > 32 || offset+int(fnameLen) > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- firstName := string(data[offset : offset+int(fnameLen)])
- if len(firstName) > 0 && firstName[len(firstName)-1] == 0 {
- firstName = firstName[:len(firstName)-1]
- }
- offset += int(fnameLen)
- // Read last name
- if offset+2 > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- lnameLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- if lnameLen > 32 || offset+int(lnameLen) > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- lastName := string(data[offset : offset+int(lnameLen)])
- if len(lastName) > 0 && lastName[len(lastName)-1] == 0 {
- lastName = lastName[:len(lastName)-1]
- }
- offset += int(lnameLen)
- // Read email
- if offset+2 > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- emailLen := binary.LittleEndian.Uint16(data[offset : offset+2])
- offset += 2
- if emailLen > 64 || offset+int(emailLen) > len(data) {
- return h.sendNotConnected(addr, seq2, uin)
- }
- email := string(data[offset : offset+int(emailLen)])
- if len(email) > 0 && email[len(email)-1] == 0 {
- email = email[:len(email)-1]
- }
- h.logger.Info("V4 registration data parsed",
- "nickname", nickname,
- "firstName", firstName,
- "lastName", lastName,
- "email", email,
- )
- // Generate a random password for the new user
- password := h.generatePassword(6)
- // Create the new user account
- newUIN, err := h.service.RegisterNewUser(ctx, nickname, firstName, lastName, email, password)
- if err != nil {
- h.logger.Error("V4 registration failed", "err", err)
- return h.sendNotConnected(addr, seq2, uin)
- }
- h.logger.Info("V4 registration successful",
- "newUIN", newUIN,
- "nickname", nickname,
- )
- // Send registration OK with the new UIN and password
- return h.sendRegistrationOK(addr, seq2, newUIN, password)
- }
- // generatePassword generates a random alphanumeric password
- func (h *V4Handler) generatePassword(length int) string {
- const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- password := make([]byte, length)
- for i := range password {
- password[i] = charset[i%len(charset)] // Simple deterministic for now
- }
- return string(password)
- }
- // handleGetDeps processes the pre-auth pseudo-login packet (0x03F2).
- // Historically called "get departments list" in iserverd (from its Users_Deps database table).
- // This is actually the main login packet in V4 - it contains the credentials
- //
- // Refactored to use service layer (AuthenticateUser) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> build response
- func (h *V4Handler) handleGetDeps(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32, data []byte) error {
- ctx := context.Background()
- // 1. Unmarshal packet to typed struct
- // V4 getdeps data format: UIN(4) + PWD_LEN(2) + PASSWORD
- if len(data) < 6 {
- h.logger.Debug("V4 getdeps packet too short", "len", len(data))
- return nil
- }
- offset := 0
- // Read UIN (4 bytes)
- dataUIN := 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("V4 getdeps (login with credentials)",
- "uin", dataUIN,
- "password_len", len(password),
- )
- // 2. Call service layer with typed request
- authReq := AuthRequest{
- UIN: dataUIN,
- Password: password,
- Version: ICQLegacyVersionV4,
- }
- loginOK, err := h.service.ValidateCredentials(ctx, authReq.UIN, authReq.Password)
- if err != nil {
- h.logger.Error("V4 getdeps authentication error", "err", err, "uin", dataUIN)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- if !loginOK {
- h.logger.Info("V4 login failed - invalid credentials", "uin", dataUIN)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- // 3. Send ACK using packet builder
- _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 4. Create session using dataUIN (the actual login UIN from the packet data),
- // not the header UIN which may be 0 or stale from a previous session.
- // TODO: parse and store direct connection info (IP, port, DC version, DC type)
- // from the V4 login packet so that presence notifications can include real
- // connection details when ICQ_LEGACY_DIRECT_CONNECTIONS includes V4.
- newSession, err := h.sessions.CreatePendingSession(dataUIN, addr, ICQLegacyVersionV4)
- if err != nil {
- h.logger.Error("failed to create V4 session", "err", err, "uin", dataUIN)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- newSession.Password = password
- // 5. Send pre-auth response (historically "departments list")
- // GETDEPS (0x03F2) is a "pseudo-login" - it validates credentials but only returns
- // the pre-auth response, NOT a full login reply.
- if err := h.sendDeptsListWithCheckcode(newSession, seq2); err != nil {
- h.logger.Error("failed to send depts list", "err", err, "uin", uin)
- return err
- }
- h.logger.Info("V4 login successful",
- "uin", uin,
- "session_id", newSession.SessionID,
- )
- return nil
- }
- // handleLogin processes login packet (0x03E8)
- //
- // In the V4 3-phase flow (FirstLogin -> GetDeps -> Login), the session was
- // already created during handleGetDeps. We reuse that session here so the
- // server seq counter is continuous (depts list got seq=0, login reply gets
- // seq=1). Creating a fresh session would reset seq to 0, and the client
- // ignores login replies with seq1=0 (same as ACK seq).
- //
- // Refactored to use service layer (AuthenticateUser) and packet builder.
- func (h *V4Handler) handleLogin(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32, data []byte) error {
- ctx := context.Background()
- // 1. Send ACK using packet builder
- _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
- // 2. Unmarshal packet to typed struct
- // V4 login data format (from matt-v4.txt):
- // TIMESTAMP(4) + TCP_PORT(4) + PWD_LEN(2) + PASSWORD + VERSION(4) + IP(4) + FLAG(1) + STATUS(4) + ...
- if len(data) < 10 {
- h.logger.Debug("V4 login packet too short", "len", len(data))
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- offset := 0
- // Skip timestamp (4 bytes)
- 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
- 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("V4 login attempt",
- "uin", uin,
- "port", port,
- "password_len", len(password),
- )
- // Read remaining fields to extract status
- // After password: UNKNOWN1(4) + IP(4) + FLAG(1) + STATUS(4)
- requestedStatus := uint32(ICQLegacyStatusOnline) // default
- if offset+4+4+1+4 <= len(data) {
- offset += 4 // skip UNKNOWN1
- offset += 4 // skip local IP
- offset += 1 // skip FLAG byte
- requestedStatus = binary.LittleEndian.Uint32(data[offset : offset+4])
- }
- h.logger.Debug("V4 login requested status",
- "uin", uin,
- "status", fmt.Sprintf("0x%08X", requestedStatus),
- )
- // 3. Call service layer with typed request
- authReq := AuthRequest{
- UIN: uin,
- Password: password,
- Status: requestedStatus,
- TCPPort: port,
- Version: ICQLegacyVersionV4,
- }
- authResult, err := h.service.AuthenticateUser(ctx, authReq)
- if err != nil {
- h.logger.Error("V4 login authentication error", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- if !authResult.Success {
- h.logger.Info("V4 login failed - invalid credentials", "uin", uin, "error_code", authResult.ErrorCode)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- // 4. Reuse the session created during handleGetDeps if it exists.
- // This preserves the server seq counter (depts list used seq=0, so
- // login reply will correctly get seq=1).
- existingSession := h.sessions.GetSession(uin)
- if existingSession != nil {
- // FirstLogin flow: session already exists from handleGetDeps.
- // Depts list was already sent there, just update metadata and
- // send the login reply.
- existingSession, err = h.sessions.AttachOSCARSession(uin, addr, authResult.oscarSession)
- if err != nil {
- h.logger.Error("failed to attach V4 OSCAR session", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- existingSession.Password = password
- existingSession.SetStatus(requestedStatus)
- loginReplyPkt := h.packetBuilder.BuildLoginReply(existingSession, seq2)
- if err := h.sender.SendToSession(existingSession, loginReplyPkt); err != nil {
- return err
- }
- h.logger.Info("V4 login successful (firstlogin flow)",
- "uin", uin,
- "session_id", existingSession.SessionID,
- )
- h.notifyContactsUserOnline(existingSession)
- if err := h.service.NotifyUserOnline(ctx, existingSession.UIN, existingSession.GetStatus()); err != nil {
- h.logger.Debug("V4 failed to notify OSCAR clients of online", "uin", existingSession.UIN, "err", err)
- }
- } else {
- // Direct login flow: client sent Login (0x03E8) without prior
- // GetDeps. Just create session and send login reply directly.
- // TODO: parse and store direct connection info from V4 login packet.
- newSession, err := h.sessions.CreateSession(uin, addr, ICQLegacyVersionV4, authResult.oscarSession)
- if err != nil {
- h.logger.Error("failed to create V4 session", "err", err, "uin", uin)
- return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
- }
- newSession.Password = password
- newSession.SetStatus(requestedStatus)
- loginReplyPkt := h.packetBuilder.BuildLoginReply(newSession, seq2)
- if err := h.sender.SendToSession(newSession, loginReplyPkt); err != nil {
- return err
- }
- h.logger.Info("V4 login successful (direct flow)",
- "uin", uin,
- "session_id", newSession.SessionID,
- )
- h.notifyContactsUserOnline(newSession)
- if err := h.service.NotifyUserOnline(ctx, newSession.UIN, newSession.GetStatus()); err != nil {
- h.logger.Debug("V4 failed to notify OSCAR clients of online", "uin", newSession.UIN, "err", err)
- }
- }
- return nil
- }
- // handlePing processes keep-alive packets
- func (h *V4Handler) handlePing(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- h.logger.Debug("V4 keep-alive received",
- "uin", uin,
- "seq1", seq1,
- "seq2", seq2,
- "addr", addr.String(),
- "session_found", session != nil,
- )
- if session == nil {
- // Try to find session by UIN instead of address
- session = h.sessions.GetSession(uin)
- if session != nil {
- h.logger.Debug("V4 session found by UIN, updating address",
- "uin", uin,
- "old_addr", session.Addr.String(),
- "new_addr", addr.String(),
- )
- // Update session address
- h.sessions.UpdateSessionAddr(uin, addr)
- } else {
- h.logger.Debug("V4 session not found by UIN either", "uin", uin)
- return h.sendNotConnected(addr, seq2, uin)
- }
- }
- session.UpdateActivity()
- return h.sendAck(addr, seq1, seq2, uin)
- }
- // handleLogoff processes logout
- // Notifies legacy contacts and OSCAR clients that this user went offline,
- // then removes the session.
- func (h *V4Handler) handleLogoff(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
- if session == nil {
- return nil
- }
- h.logger.Info("V4 logout", "uin", uin)
- // Notify legacy 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("V4 failed to notify OSCAR clients of offline", "uin", uin, "err", err)
- }
- h.sessions.RemoveSession(uin)
- return nil
- }
- // handleContactList processes contact list
- //
- // Refactored to use service layer (ProcessContactList) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> send notifications
- func (h *V4Handler) handleContactList(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
- req, err := h.parseContactListPacket(data, uin)
- if err != nil {
- h.logger.Debug("V4 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("V4 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
- result, err := h.service.ProcessContactList(ctx, session.Instance, req)
- if err != nil {
- h.logger.Error("V4 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 using packet builder
- 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 {
- onlinePkt := h.packetBuilder.BuildUserOnline(
- session.NextServerSeqNum(),
- contact.UIN,
- contact.Status,
- )
- // Set the recipient UIN in the packet (offset 8-11)
- binary.LittleEndian.PutUint32(onlinePkt[8:12], session.UIN)
- _ = h.sender.SendToSession(session, onlinePkt)
- }
- }
- }
- 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 V4 contact list packet into a typed ContactListRequest struct.
- // V4 format (from licq CPU_ContactList): COUNT(1) + UIN(4)*COUNT
- // Note: V4 does NOT have a timestamp prefix - the data starts immediately with the count byte.
- func (h *V4Handler) parseContactListPacket(data []byte, ownerUIN uint32) (ContactListRequest, error) {
- req := ContactListRequest{
- UIN: ownerUIN,
- Contacts: make([]uint32, 0),
- }
- if len(data) < 1 {
- return req, fmt.Errorf("contact list packet too short: %d bytes", len(data))
- }
- // Contact count (1 byte) - no timestamp prefix in V4
- offset := 0
- 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
- }
- // handleSetStatus processes status change
- //
- // Refactored to use service layer (ProcessStatusChange) and packet builder.
- // Following the OSCAR pattern: unmarshal -> call service -> broadcast
- func (h *V4Handler) 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
- // V4 status change format (from licq CPU_SetStatus): STATUS(4)
- // Note: V4 does NOT have a timestamp prefix - data starts with the status value.
- if len(data) < 4 {
- return nil
- }
- newStatus := binary.LittleEndian.Uint32(data[0:4])
- oldStatus := session.GetStatus()
- h.logger.Debug("V4 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("V4 failed to process status change", "err", err)
- return nil
- }
- h.logger.Debug("V4 status change processed",
- "uin", uin,
- "notify_count", len(statusResult.NotifyTargets),
- )
- // 4. Broadcast status change to contacts
- // 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 {
- _ = h.dispatcher.SendStatusChange(targetSession, uin, newStatus)
- } else {
- _ = h.sendUserStatus(targetSession, uin, newStatus)
- }
- }
- }
- return nil
- }
- // handleMessage processes through-server messages (0x010E, 0x0456)
- // 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 *V4Handler) handleMessage(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
- req, err := h.parseMessagePacket(data, uin)
- if err != nil {
- h.logger.Debug("V4 message parse error", "from", uin, "err", err)
- return nil
- }
- h.logger.Debug("V4 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
- result, err := h.service.ProcessMessage(ctx, session, req)
- if err != nil {
- h.logger.Error("V4 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 {
- _ = h.dispatcher.SendOnlineMessage(targetSession, req.FromUIN, req.MsgType, req.Message)
- } else {
- msgPkt := h.packetBuilder.BuildOnlineMessage(
- targetSession.NextServerSeqNum(),
- req.FromUIN,
- req.MsgType,
- req.Message,
- )
- // Set the recipient UIN in the packet (offset 8-11)
- binary.LittleEndian.PutUint32(msgPkt[8:12], targetSession.UIN)
- _ = h.sender.SendToSession(targetSession, msgPkt)
- }
- h.logger.Debug("V4 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.Debug("V4 target user offline, message stored",
- "from", req.FromUIN,
- "to", req.ToUIN,
- )
- } else {
- h.logger.Debug("V4 target user offline, message not delivered",
- "from", req.FromUIN,
- "to", req.ToUIN,
- )
- }
- return nil
- }
- // parseMessagePacket parses a V4 message packet into a typed MessageRequest struct.
- // V4 format (from licq CPU_ThroughServer): TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- // Note: V4 does NOT have a timestamp prefix - data starts with the destination UIN.
- func (h *V4Handler) parseMessagePacket(data []byte, fromUIN uint32) (MessageRequest, error) {
- req := MessageRequest{
- FromUIN: fromUIN,
- }
- if len(data) < 8 {
- return req, fmt.Errorf("message packet too short: %d bytes", len(data))
- }
- offset := 0
- // 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)
- // Sends "you were added" notification to target user
- func (h *V4Handler) handleUserAdd(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // V4 format (from licq CPU_AddUser): TARGET_UIN(4)
- // Note: V4 does NOT have a timestamp prefix.
- if len(data) < 4 {
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[0:4])
- h.logger.Debug("V4 user add",
- "from", uin,
- "target", targetUIN,
- )
- contacts := session.GetContactList()
- contacts = append(contacts, targetUIN)
- session.SetContactList(contacts)
- ctx := context.Background()
- if _, err := h.service.ProcessUserAdd(ctx, session.Instance, UserAddRequest{
- FromUIN: uin,
- TargetUIN: targetUIN,
- }); err != nil {
- h.logger.Debug("V4 user add service call failed", "err", err)
- }
- targetSession := h.sessions.GetSession(targetUIN)
- if targetSession != nil {
- // Send target's online status to the user who added them
- if h.dispatcher != nil {
- _ = h.dispatcher.SendUserOnline(session, targetUIN, targetSession.GetStatus())
- } else {
- _ = h.sendUserOnline(session, targetUIN, targetSession.GetStatus())
- }
- // Send "you were added" notification to target user
- // Message format: nick#FE#first#FE#last#FE#email#FE#auth
- youWereAddedMsg := fmt.Sprintf("%d\xFE\xFE\xFE\xFE0", uin)
- if h.dispatcher != nil {
- _ = h.dispatcher.SendOnlineMessage(targetSession, uin, ICQLegacyMsgAdded, youWereAddedMsg)
- } else {
- _ = h.sendOnlineMessage(targetSession, uin, 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. Without
- // this, the target's client shows the adder as offline
- // even though they're connected.
- if h.dispatcher != nil {
- _ = h.dispatcher.SendUserOnline(targetSession, uin, session.GetStatus())
- } else {
- _ = h.sendUserOnline(targetSession, uin, session.GetStatus())
- }
- h.logger.Debug("V4 sent 'you were added' notification",
- "from", uin,
- "to", targetUIN,
- )
- }
- return nil
- }
- // handleGetInfo processes user info request
- func (h *V4Handler) handleGetInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // V4 format (from licq CPU_GetUserBasicInfo): TARGET_UIN(4)
- // Note: V4 does NOT have a timestamp/subsequence prefix (unlike V2).
- if len(data) < 4 {
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[0:4])
- h.logger.Debug("V4 get info request",
- "from", uin,
- "target", targetUIN,
- )
- // Send basic info
- _ = h.sendBasicInfo(session, seq2, targetUIN)
- return nil
- }
- // handleInfoReq processes old-style user info request (0x0460)
- // Format: TARGET_UIN(4) - no timestamp prefix
- // Response: 0x0118 (INFO_REPLY)
- func (h *V4Handler) handleInfoReq(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse - format: TARGET_UIN(4) only
- if len(data) < 4 {
- h.logger.Debug("V4 info request too short", "len", len(data))
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[0:4])
- h.logger.Debug("V4 info request (0x0460)",
- "from", uin,
- "target", targetUIN,
- )
- // Send basic info response (0x0118 - ICQLegacySrvInfoReply)
- return h.sendBasicInfoResponse(session, seq2, targetUIN)
- }
- // handleExtInfoReq processes extended info request (0x046A)
- // Format: TARGET_UIN(4) - no timestamp prefix
- // Response: 0x0122 (EXT_INFO_REPLY) - NOT 0x0118!
- func (h *V4Handler) handleExtInfoReq(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse - format: TARGET_UIN(4) only
- if len(data) < 4 {
- h.logger.Debug("V4 ext info request too short", "len", len(data))
- return nil
- }
- targetUIN := binary.LittleEndian.Uint32(data[0:4])
- h.logger.Debug("V4 ext info request (0x046A)",
- "from", uin,
- "target", targetUIN,
- )
- // Send extended info response (0x0122 - ICQLegacySrvExtInfoReply)
- return h.sendExtInfoResponse(session, seq2, targetUIN)
- }
- // handleSearchByUIN processes old-style search by UIN (0x041A)
- // V4 format: TARGET_UIN(4) - no timestamp/subsequence prefix
- // Response: 0x008C (SEARCH_FOUND) + 0x00A0 (SEARCH_DONE)
- // From licq ProcessUdpPacket case ICQ_CMDxRCV_SEARCHxFOUND:
- //
- // UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
- func (h *V4Handler) handleSearchByUIN(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) < 4 {
- h.logger.Debug("V4 search by UIN - data too short", "uin", uin)
- return h.sendSearchEnd(session, seq2, false)
- }
- ctx := context.Background()
- targetUIN := binary.LittleEndian.Uint32(data[0:4])
- h.logger.Info("V4 search by UIN",
- "uin", uin,
- "target_uin", targetUIN,
- )
- result, err := h.service.SearchByUIN(ctx, targetUIN)
- if err != nil {
- h.logger.Debug("V4 search by UIN - error", "target_uin", targetUIN, "err", err)
- return h.sendSearchEnd(session, seq2, false)
- }
- if result == nil {
- h.logger.Debug("V4 search by UIN - not found", "target_uin", targetUIN)
- return h.sendSearchEnd(session, seq2, false)
- }
- h.logger.Info("V4 search by UIN - found",
- "target_uin", targetUIN,
- "nickname", result.Nickname,
- )
- _ = h.sendSearchFound(session, seq2, result)
- return h.sendSearchEnd(session, seq2, false)
- }
- // handleSearchByName processes old-style search by name/email (0x0424)
- // V4 format (from licq CPU_StartSearch): NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST + LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL
- // Response: 0x008C (SEARCH_FOUND) for each result + 0x00A0 (SEARCH_DONE)
- func (h *V4Handler) handleSearchByName(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Info("V4 search by name", "uin", uin, "data_len", len(data))
- // TODO: parse search parameters and call h.service.SearchByName
- return h.sendSearchEnd(session, seq2, false)
- }
- // sendSearchFound sends a search result (0x008C) to the session.
- // V3 server packet format with checkcode.
- // Data format (from licq): UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST +
- //
- // LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
- func (h *V4Handler) sendSearchFound(session *LegacySession, seq2 uint16, result *LegacyUserSearchResult) error {
- if result == nil {
- return nil
- }
- 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)
- if nick == "" {
- nick = fmt.Sprintf("%d", result.UIN)
- }
- // header(16) + UIN(4) + nick(2+len+1) + first(2+len+1) + last(2+len+1) + email(2+len+1) + auth(1)
- 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:], ICQLegacySrvSearchFound) // 0x008C
- 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
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // UIN of found user
- binary.LittleEndian.PutUint32(pkt[offset:], result.UIN)
- offset += 4
- // Nick (length-prefixed, null-terminated)
- 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] = result.AuthRequired
- offset++
- h.logger.Debug("sending V4 search found (0x008C)",
- "to", session.UIN,
- "found_uin", result.UIN,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendSearchEnd sends search done marker (0x00A0) to the session.
- // V3 server packet format with checkcode.
- // Data format (from licq): MORE(1)
- func (h *V4Handler) sendSearchEnd(session *LegacySession, seq2 uint16, more bool) error {
- pkt := make([]byte, 17)
- offset := 0
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvSearchDone) // 0x00A0
- 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
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- var moreByte byte
- if more {
- moreByte = 0x01
- }
- pkt[offset] = moreByte
- offset++
- h.logger.Debug("sending V4 search done (0x00A0)",
- "to", session.UIN,
- "more", more,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // handleUpdateBasic processes V4 update basic info (0x050A)
- // V4 format (from licq CPU_UpdatePersonalBasicInfo, no subsequence prefix):
- //
- // NICK_LEN(2) + NICK + FNAME_LEN(2) + FNAME + LNAME_LEN(2) + LNAME + EMAIL_LEN(2) + EMAIL + AUTH(1)
- //
- // Response: 0x01E0 (ICQLegacySrvUpdatedBasicV4) on success, 0x01EA on failure
- func (h *V4Handler) handleUpdateBasic(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse: NICK_LEN(2)+NICK + FNAME_LEN(2)+FNAME + LNAME_LEN(2)+LNAME + EMAIL_LEN(2)+EMAIL + AUTH(1)
- if len(data) >= 4 {
- r := bytes.NewReader(data)
- alias, _ := ParseLegacyString(r, true)
- first, _ := ParseLegacyString(r, true)
- last, _ := ParseLegacyString(r, true)
- email, _ := ParseLegacyString(r, true)
- var auth uint8
- _ = binary.Read(r, binary.LittleEndian, &auth)
- ctx := context.Background()
- info := state.ICQBasicInfo{
- Nickname: alias,
- FirstName: first,
- LastName: last,
- EmailAddress: email,
- }
- if err := h.service.UpdateBasicInfo(ctx, uin, info); err != nil {
- h.logger.Error("V4 update basic failed", "uin", uin, "err", err)
- }
- if err := h.service.SetAuthMode(ctx, uin, auth == 1); err != nil {
- h.logger.Error("V4 set auth mode failed", "uin", uin, "err", err)
- }
- }
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvUpdatedBasicV4)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- return h.sender.SendToSession(session, pkt)
- }
- // handleUpdateDetail processes V4 update extended info (0x04B0)
- // V4 format (from licq CPU_UpdatePersonalExtInfo, no subsequence prefix):
- //
- // CITY_LEN(2) + CITY + COUNTRY(2) + TIMEZONE(1) + STATE_LEN(2) + STATE +
- // AGE(2) + SEX(1) + PHONE_LEN(2) + PHONE + HOMEPAGE_LEN(2) + HOMEPAGE +
- // ABOUT_LEN(2) + ABOUT + ZIPCODE(4)
- //
- // Response: 0x00C8 on success, 0x00D2 on failure
- func (h *V4Handler) handleUpdateDetail(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- // Parse: CITY_LEN(2)+CITY + COUNTRY(2) + TIMEZONE(1) + STATE_LEN(2)+STATE +
- // AGE(2) + SEX(1) + PHONE_LEN(2)+PHONE + HOMEPAGE_LEN(2)+HOMEPAGE +
- // ABOUT_LEN(2)+ABOUT + ZIPCODE(4)
- if len(data) >= 4 {
- r := bytes.NewReader(data)
- city, _ := ParseLegacyString(r, true)
- var country uint16
- _ = binary.Read(r, binary.LittleEndian, &country)
- var timezone uint8
- _ = binary.Read(r, binary.LittleEndian, &timezone)
- st, _ := ParseLegacyString(r, true)
- var age uint16
- _ = binary.Read(r, binary.LittleEndian, &age)
- var sex uint8
- _ = binary.Read(r, binary.LittleEndian, &sex)
- phone, _ := ParseLegacyString(r, true)
- homepage, _ := ParseLegacyString(r, true)
- about, _ := ParseLegacyString(r, true)
- ctx := context.Background()
- // Read existing basic info to avoid overwriting nick/first/last/email
- existing, err := h.service.GetFullUserInfo(ctx, uin)
- if err == nil && existing != nil {
- existing.ICQInfo.Basic.City = city
- existing.ICQInfo.Basic.CountryCode = country
- existing.ICQInfo.Basic.State = st
- existing.ICQInfo.Basic.Phone = phone
- if err := h.service.UpdateBasicInfo(ctx, uin, existing.ICQInfo.Basic); err != nil {
- h.logger.Error("V4 update detail basic failed", "uin", uin, "err", err)
- }
- }
- // Merge more info to avoid overwriting birthday/languages
- if existing != nil {
- existing.ICQInfo.More.Gender = uint16(sex)
- existing.ICQInfo.More.HomePageAddr = homepage
- if err := h.service.UpdateMoreInfo(ctx, uin, existing.ICQInfo.More); err != nil {
- h.logger.Error("V4 update detail more failed", "uin", uin, "err", err)
- }
- }
- if about != "" {
- if err := h.service.SetNotes(ctx, uin, about); err != nil {
- h.logger.Error("V4 update detail about failed", "uin", uin, "err", err)
- }
- }
- }
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvUpdatedDetail)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], seq2)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- return h.sender.SendToSession(session, pkt)
- }
- // handleOfflineMsgReq processes offline message request
- // Delivers stored offline messages then sends "done" marker.
- // From licq: client sends 0x044C, server responds with 0x00DC for each
- // offline message, then 0x00E6 (SYS_MSG_DONE).
- // Offline message format (from licq ProcessUdpPacket case ICQ_CMDxRCV_SYSxMSGxOFFLINE):
- //
- // UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MIN(1) + MSG_TYPE(2) + MSG_LEN(2) + MSG
- func (h *V4Handler) handleOfflineMsgReq(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
- if session == nil {
- return nil
- }
- _ = h.sendAck(session.Addr, seq1, seq2, uin)
- h.logger.Debug("V4 offline message request", "uin", uin)
- ctx := context.Background()
- // Retrieve stored offline messages from the service layer
- messages, err := h.service.GetOfflineMessages(ctx, uin)
- if err != nil {
- h.logger.Debug("V4 failed to get offline messages", "uin", uin, "err", err)
- // Still send done even on error
- return h.sendOfflineMsgDone(session, seq2)
- }
- // Send each offline message as SYS_MSG_OFFLINE (0x00DC)
- for _, msg := range messages {
- _ = h.sendOfflineMessage(session, msg)
- }
- // Acknowledge (delete) offline messages after delivery
- if len(messages) > 0 {
- if err := h.service.AckOfflineMessages(ctx, uin); err != nil {
- h.logger.Debug("V4 failed to ack offline messages", "uin", uin, "err", err)
- }
- }
- return h.sendOfflineMsgDone(session, seq2)
- }
- // sendOfflineMessage sends a single offline message (0x00DC) to the session.
- // Format: header(16) + FROM_UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MIN(1) + MSG_TYPE(2) + MSG_LEN(2) + MSG
- func (h *V4Handler) sendOfflineMessage(session *LegacySession, msg LegacyOfflineMessage) error {
- msgBytes := []byte(msg.Message)
- pktSize := 16 + 4 + 2 + 1 + 1 + 1 + 1 + 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:], ICQLegacySrvSysMsgOffline) // 0x00DC
- 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
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // From UIN
- binary.LittleEndian.PutUint32(pkt[offset:], msg.FromUIN)
- offset += 4
- // Timestamp: YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MIN(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
- offset++
- h.logger.Debug("sending V4 offline message",
- "to", session.UIN,
- "from", msg.FromUIN,
- "type", fmt.Sprintf("0x%04X", msg.MsgType),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // V4 packet sending helpers
- // Server sends V3 format packets to V4 clients (confirmed in documentation)
- // V3 packets need a checkcode at the end
- // calculateV4Checkcode calculates the checkcode for server packets to V4 clients
- // Based on reverse engineering of ICQ 98a client (encrypt_v4_packet and validate_v4_checkcode)
- //
- // The client validates checkcode as follows:
- //
- // checkA = (byte[8] << 24) | (byte[4] << 16) | (byte[2] << 8) | byte[6]
- // result = checkA ^ checkcode
- // valid if: (result >> 16) & 0xFF == ~packet[result >> 24]
- // and: result & 0xFF == ~table[(result >> 8) & 0xFF]
- //
- // So we construct checkcode such that after XOR with checkA, the result contains:
- //
- // byte 3 (bits 24-31): packetOfs - offset into packet
- // byte 2 (bits 16-23): ~packet[packetOfs] - inverted byte at that offset
- // byte 1 (bits 8-15): tableOfs - offset into table
- // byte 0 (bits 0-7): ~table[tableOfs] - inverted byte at that table offset
- func (h *V4Handler) calculateV4Checkcode(packet []byte) uint32 {
- if len(packet) < 12 {
- return 0
- }
- // checkA: pack bytes 8, 4, 2, 6 from packet (MSB to LSB)
- checkA := uint32(packet[8])<<24 | uint32(packet[4])<<16 | uint32(packet[2])<<8 | uint32(packet[6])
- // Choose offsets (we use fixed values, but could be random)
- // Avoid offset 0x10-0x13 (checkcode position) - use offset 4 (seq1 low byte)
- packetOfs := byte(4)
- tableOfs := byte(0)
- // Get values and INVERT them (this is what the client expects)
- packetVal := ^packet[packetOfs] // inverted
- tableVal := ^V4Table[tableOfs] // inverted
- // Build checkB with inverted values
- checkB := uint32(packetOfs)<<24 | uint32(packetVal)<<16 | uint32(tableOfs)<<8 | uint32(tableVal)
- // checkcode = checkA ^ checkB
- result := checkA ^ checkB
- h.logger.Debug("V4 checkcode calculation",
- "byte8", fmt.Sprintf("0x%02X", packet[8]),
- "byte4", fmt.Sprintf("0x%02X", packet[4]),
- "byte2", fmt.Sprintf("0x%02X", packet[2]),
- "byte6", fmt.Sprintf("0x%02X", packet[6]),
- "checkA", fmt.Sprintf("0x%08X", checkA),
- "packetOfs", packetOfs,
- "packetVal_inverted", fmt.Sprintf("0x%02X", packetVal),
- "tableOfs", tableOfs,
- "tableVal_inverted", fmt.Sprintf("0x%02X", tableVal),
- "checkB", fmt.Sprintf("0x%08X", checkB),
- "checkcode", fmt.Sprintf("0x%08X", result),
- )
- return result
- }
- // sendAck sends ACK packet in V3 format with checkcode
- func (h *V4Handler) sendAck(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
- 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)
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- h.logger.Debug("sending V4 ACK",
- "to", addr.String(),
- "seq1", seq1,
- "seq2", seq2,
- "uin", uin,
- "checkcode", fmt.Sprintf("0x%08X", checkcode),
- "hex", fmt.Sprintf("%X", pkt),
- )
- return h.sender.SendPacket(addr, pkt)
- }
- // sendNotConnected sends not connected error
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + ZERO(4)
- func (h *V4Handler) 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) // Zero field
- return h.sender.SendPacket(addr, pkt)
- }
- // notifyContactsUserOnline notifies all contacts who have this user in their list that we're online
- func (h *V4Handler) notifyContactsUserOnline(session *LegacySession) {
- if h.dispatcher == nil {
- return
- }
- contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
- h.logger.Debug("V4 notifying contacts of user online",
- "uin", session.UIN,
- "contacts_to_notify", contactsToNotify,
- )
- for _, contactUIN := range contactsToNotify {
- contactSession := h.sessions.GetSession(contactUIN)
- if contactSession != nil {
- _ = h.dispatcher.SendUserOnline(contactSession, session.UIN, session.GetStatus())
- }
- }
- }
- // notifyContactsUserOffline notifies all contacts who have this user in their list that we're offline
- func (h *V4Handler) notifyContactsUserOffline(session *LegacySession) {
- if h.dispatcher == nil {
- return
- }
- contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
- h.logger.Debug("V4 notifying contacts of user offline",
- "uin", session.UIN,
- "contacts_to_notify", contactsToNotify,
- )
- for _, contactUIN := range contactsToNotify {
- contactSession := h.sessions.GetSession(contactUIN)
- if contactSession != nil {
- _ = h.dispatcher.SendUserOffline(contactSession, session.UIN)
- }
- }
- }
- // sendUserOnline sends user online notification
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // From iserverd v3_send_user_online() - V3 and V4 share the same server packet format.
- // Data: UIN(4) + IP(4) + PORT(4) + REAL_IP(4) + DC_TYPE(1) + STATUS(2) + ESTATUS(2) + DCVER(2) + UNKNOWN(2)
- // Total data: 25 bytes, total packet: 41 bytes
- func (h *V4Handler) sendUserOnline(session *LegacySession, uin uint32, status uint32) error {
- 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)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // UIN of user who came online
- binary.LittleEndian.PutUint32(pkt[offset:], uin)
- offset += 4
- // IP address (0 for privacy - V3 clients crash on non-V3 direct connect)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // TCP port (4 bytes, 0 for privacy)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // Internal/real IP (4 bytes, 0 for privacy)
- binary.LittleEndian.PutUint32(pkt[offset:], 0)
- offset += 4
- // DC type (1 byte)
- pkt[offset] = 0
- offset++
- // Status (low 16 bits)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status&0xFFFF))
- offset += 2
- // Extended status (high 16 bits)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(status>>16))
- offset += 2
- // DC version (2 bytes)
- binary.LittleEndian.PutUint16(pkt[offset:], 0)
- offset += 2
- // Unknown (2 bytes)
- binary.LittleEndian.PutUint16(pkt[offset:], 0)
- offset += 2
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendDeptsListWithCheckcode sends the pre-auth response with proper checkcode for V4 clients.
- // Historically called "departments list" in iserverd (from its Users_Deps database table).
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // From iserverd v3_send_depslist() - this completes the login sequence for V4 clients
- func (h *V4Handler) sendDeptsListWithCheckcode(session *LegacySession, seq2 uint16) error {
- // Packet: header(16) + DEPLIST_VERSION(4) + COUNT(4) + UNKNOWN(4) = 28 bytes
- pkt := make([]byte, 28)
- offset := 0
- // V3 Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserDepsList) // 0x0032
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum()) // server seq1
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2) // client's seq2
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
- offset += 4
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // Pre-auth response data (simplified - empty list)
- // DEPLIST_VERSION(4) + COUNT(4) + UNKNOWN(4)
- binary.LittleEndian.PutUint32(pkt[offset:], 1) // version
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0) // count (empty list)
- offset += 4
- binary.LittleEndian.PutUint32(pkt[offset:], 0x002A0002) // unknown fields from iserverd
- offset += 4
- h.logger.Debug("sending V4 depts list (0x0032)",
- "uin", session.UIN,
- "seq2", seq2,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendDeptsListUnsolicited sends the pre-auth response as an unsolicited notification (seq1=0).
- // Historically called "departments list" in iserverd.
- // This is used during direct login (0x03E8) to set the client's internal flag before
- // sending the login reply. Using seq1=0 ensures the login reply can still get seq1=1.
- //
- // sendRegisterInfo sends registration info (admin notes) to the client
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // From iserverd v3_send_registration_info(): sends 0x037A with admin notes
- func (h *V4Handler) sendRegisterInfo(addr *net.UDPAddr, seq2 uint16, uin uint32) error {
- // Admin notes message - tells the user about registration
- adminNotes := "Welcome to Open OSCAR Server!\nRegistration is enabled.\x00"
- // Build packet
- // Header (16 bytes) + NOTES_LEN(2) + NOTES + REG_ENABLED(1) + UNKNOWN(4)
- pktSize := 16 + 2 + len(adminNotes) + 1 + 4
- pkt := make([]byte, pktSize)
- offset := 0
- // V3 Header
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvRegisterInfo)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq1 = 0 for server-initiated
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], uin)
- offset += 4
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // Admin notes length and string
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(adminNotes)))
- offset += 2
- copy(pkt[offset:], adminNotes)
- offset += len(adminNotes)
- // Registration enabled flag (1 = enabled)
- pkt[offset] = 0x01
- offset++
- // Unknown fields (from iserverd: 0x0002, 0x002A)
- binary.LittleEndian.PutUint16(pkt[offset:], 0x0002)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0x002A)
- offset += 2
- h.logger.Debug("sending V4 registration info",
- "uin", uin,
- "seq2", seq2,
- "admin_notes_len", len(adminNotes),
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendPacket(addr, pkt[:offset])
- }
- // sendRegistrationOK sends registration success with the new UIN
- // V4 client (licq) expects ICQ_CMDxRCV_NEWxUIN (0x0046) - NOT 0x0384.
- // The client reads the new UIN from the header's UIN field (offset 8-11),
- // then calls icqLogon() to start the login sequence.
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4)
- func (h *V4Handler) sendRegistrationOK(addr *net.UDPAddr, seq2 uint16, newUIN uint32, password string) error {
- pkt := make([]byte, 16)
- offset := 0
- // V3 Header - UIN field contains the NEW UIN
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvNewUIN) // 0x0046
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq1 = 0 for server-initiated
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], seq2)
- offset += 2
- binary.LittleEndian.PutUint32(pkt[offset:], newUIN) // NEW UIN in header
- offset += 4
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- h.logger.Info("sending V4 new UIN (0x0046)",
- "newUIN", newUIN,
- "seq2", seq2,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendPacket(addr, pkt[:offset])
- }
- // sendBasicInfo sends basic user info response
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // Data format: TARGET_UIN(4) + NICK_LEN(2) + NICK + FNAME_LEN(2) + FNAME + LNAME_LEN(2) + LNAME + EMAIL_LEN(2) + EMAIL + AUTH(1)
- func (h *V4Handler) sendBasicInfo(session *LegacySession, seq2 uint16, targetUIN uint32) error {
- nick := fmt.Sprintf("%d", targetUIN)
- // header(16) + TARGET_UIN(4) + nick(2+len+1) + fname(2+1) + lname(2+1) + email(2+1) + auth(1)
- pktSize := 16 + 4 + 2 + len(nick) + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 1
- pkt := make([]byte, pktSize)
- offset := 0
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvInfoReply) // 0x0118
- 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
- // Calculate checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // TARGET_UIN - the UIN of the user whose info this is
- binary.LittleEndian.PutUint32(pkt[offset:], targetUIN)
- offset += 4
- // Nick
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(nick)+1))
- offset += 2
- copy(pkt[offset:], nick)
- offset += len(nick)
- pkt[offset] = 0
- offset++
- // First name (empty)
- binary.LittleEndian.PutUint16(pkt[offset:], 1)
- offset += 2
- pkt[offset] = 0
- offset++
- // Last name (empty)
- binary.LittleEndian.PutUint16(pkt[offset:], 1)
- offset += 2
- pkt[offset] = 0
- offset++
- // Email (empty)
- binary.LittleEndian.PutUint16(pkt[offset:], 1)
- offset += 2
- pkt[offset] = 0
- offset++
- // Auth flag
- pkt[offset] = 0
- offset++
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendBasicInfoResponse sends basic user info response (0x0118) with checkcode
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // Data format (verified via Ghidra RE of ICQ98a client handler FUN_00429b22):
- //
- // TARGET_UIN(4) + NICK_LEN(2) + NICK + FNAME_LEN(2) + FNAME + LNAME_LEN(2) + LNAME + EMAIL_LEN(2) + EMAIL + STATUS(1) + AUTH(1)
- //
- // NOTE: Client expects 0x0118 (ICQLegacySrvInfoReply), NOT 0x02E4 - verified via Ghidra RE
- // NOTE: There are TWO bytes at the end - STATUS and AUTH - verified via Ghidra RE of FUN_00429b22
- func (h *V4Handler) sendBasicInfoResponse(session *LegacySession, seq2 uint16, targetUIN uint32) error {
- ctx := context.Background()
- // Try to get actual user info from the service
- nick := fmt.Sprintf("%d", targetUIN)
- firstName := ""
- lastName := ""
- email := ""
- statusByte := byte(0) // Status byte (stored at offset 0xd1 in client user object)
- authMode := byte(0) // Auth mode (stored at offset 0x911 in client user object)
- info, err := h.service.GetUserInfo(ctx, targetUIN)
- if err == nil && info != nil {
- if info.Nickname != "" {
- nick = info.Nickname
- }
- firstName = info.FirstName
- lastName = info.LastName
- email = info.Email
- }
- // Truncate fields to prevent crashing old ICQ clients
- nick = truncateField(nick, 20, h.logger, "nickname", targetUIN)
- firstName = truncateField(firstName, 64, h.logger, "first_name", targetUIN)
- lastName = truncateField(lastName, 64, h.logger, "last_name", targetUIN)
- email = truncateField(email, 64, h.logger, "email", targetUIN)
- // Calculate packet size: header(16) + TARGET_UIN(4) + nick(2+len+1) + fname(2+len+1) + lname(2+len+1) + email(2+len+1) + status(1) + auth(1)
- pktSize := 16 + 4 + (2 + len(nick) + 1) + (2 + len(firstName) + 1) + (2 + len(lastName) + 1) + (2 + len(email) + 1) + 1 + 1
- pkt := make([]byte, pktSize)
- offset := 0
- // V3 Header with checkcode
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvInfoReply) // 0x0118
- 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
- // Calculate and add checkcode - client validates this!
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // TARGET_UIN - the UIN of the user whose info this is (required by client!)
- binary.LittleEndian.PutUint32(pkt[offset:], targetUIN)
- offset += 4
- // NickName (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(nick)+1))
- offset += 2
- copy(pkt[offset:], nick)
- offset += len(nick)
- pkt[offset] = 0
- offset++
- // First name (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(firstName)+1))
- offset += 2
- copy(pkt[offset:], firstName)
- offset += len(firstName)
- pkt[offset] = 0
- offset++
- // Last name (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(lastName)+1))
- offset += 2
- copy(pkt[offset:], lastName)
- offset += len(lastName)
- pkt[offset] = 0
- offset++
- // Email (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(email)+1))
- offset += 2
- copy(pkt[offset:], email)
- offset += len(email)
- pkt[offset] = 0
- offset++
- // Status byte (read first by client, stored at offset 0xd1)
- pkt[offset] = statusByte
- offset++
- // Auth mode (read second by client, stored at offset 0x911)
- pkt[offset] = authMode
- offset++
- h.logger.Debug("sending V4 basic info response",
- "to", session.UIN,
- "target", targetUIN,
- "nick", nick,
- "seq2", seq2,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendExtInfoResponse sends extended user info response (0x0122) with checkcode
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // Data format (verified via Ghidra RE of ICQ98a client handler FUN_00429c84):
- //
- // TARGET_UIN(4) + CITY_LEN(2) + CITY + COUNTRY(2) + TIMEZONE(1) +
- // STATE_LEN(2) + STATE + AGE(2) + GENDER(1) +
- // PHONE_LEN(2) + PHONE + HOMEPAGE_LEN(2) + HOMEPAGE + ABOUT_LEN(2) + ABOUT + ZIPCODE(4)
- func (h *V4Handler) sendExtInfoResponse(session *LegacySession, seq2 uint16, targetUIN uint32) error {
- ctx := context.Background()
- // Default values
- city := ""
- country := uint16(0)
- timezone := byte(0)
- state := ""
- age := uint16(0)
- gender := byte(0)
- phone := ""
- homepage := ""
- about := ""
- zipcode := uint32(0)
- // Try to get full user info from the service
- user, err := h.service.GetFullUserInfo(ctx, targetUIN)
- if err == nil && user != nil {
- city = user.ICQInfo.Basic.City
- country = user.ICQInfo.Basic.CountryCode
- timezone = user.ICQInfo.Basic.GMTOffset
- state = user.ICQInfo.Basic.State
- phone = user.ICQInfo.Basic.Phone
- homepage = user.ICQInfo.More.HomePageAddr
- about = user.ICQInfo.Notes.Notes
- // Calculate age from birth year
- if user.ICQInfo.More.BirthYear > 0 {
- currentYear := uint16(2026) // Current year
- age = currentYear - user.ICQInfo.More.BirthYear
- }
- // Gender (ICQ uses 1=female, 2=male, 0=unspecified)
- gender = byte(user.ICQInfo.More.Gender)
- // Zip code - stored as string, sent as uint32 in V3/V4 protocol
- if user.ICQInfo.Basic.ZIPCode != "" {
- if z, err := strconv.ParseUint(user.ICQInfo.Basic.ZIPCode, 10, 32); err == nil {
- zipcode = uint32(z)
- }
- }
- }
- // Truncate fields to prevent crashing old ICQ clients
- city = truncateField(city, 64, h.logger, "city", targetUIN)
- state = truncateField(state, 64, h.logger, "state", targetUIN)
- phone = truncateField(phone, 30, h.logger, "phone", targetUIN)
- homepage = truncateField(homepage, 127, h.logger, "homepage", targetUIN)
- about = truncateField(about, 450, h.logger, "about", targetUIN)
- // Calculate packet size: header(16) + TARGET_UIN(4) + city(2+len+1) + country(2) + timezone(1) +
- // state(2+len+1) + age(2) + gender(1) + phone(2+len+1) + homepage(2+len+1) + about(2+len+1) + unknown(4)
- pktSize := 16 + 4 + (2 + len(city) + 1) + 2 + 1 + (2 + len(state) + 1) + 2 + 1 +
- (2 + len(phone) + 1) + (2 + len(homepage) + 1) + (2 + len(about) + 1) + 4
- pkt := make([]byte, pktSize)
- offset := 0
- // V3 Header with checkcode
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
- offset += 2
- binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvExtInfoReply) // 0x0122
- 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
- // Calculate and add checkcode - client validates this!
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], checkcode)
- offset += 4
- // TARGET_UIN
- binary.LittleEndian.PutUint32(pkt[offset:], targetUIN)
- offset += 4
- // City (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(city)+1))
- offset += 2
- copy(pkt[offset:], city)
- offset += len(city)
- pkt[offset] = 0
- offset++
- // Country code
- binary.LittleEndian.PutUint16(pkt[offset:], country)
- offset += 2
- // Timezone
- pkt[offset] = timezone
- offset++
- // State (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(state)+1))
- offset += 2
- copy(pkt[offset:], state)
- offset += len(state)
- pkt[offset] = 0
- offset++
- // Age
- binary.LittleEndian.PutUint16(pkt[offset:], age)
- offset += 2
- // Gender
- pkt[offset] = gender
- offset++
- // Phone (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(phone)+1))
- offset += 2
- copy(pkt[offset:], phone)
- offset += len(phone)
- pkt[offset] = 0
- offset++
- // Homepage (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(homepage)+1))
- offset += 2
- copy(pkt[offset:], homepage)
- offset += len(homepage)
- pkt[offset] = 0
- offset++
- // About (length + string + null)
- binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(about)+1))
- offset += 2
- copy(pkt[offset:], about)
- offset += len(about)
- pkt[offset] = 0
- offset++
- // Zip code (DWORD at end - verified via licq source: packet >> zipcode)
- binary.LittleEndian.PutUint32(pkt[offset:], zipcode)
- offset += 4
- h.logger.Debug("sending V4 ext info response",
- "to", session.UIN,
- "target", targetUIN,
- "seq2", seq2,
- "hex", fmt.Sprintf("%X", pkt[:offset]),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendOfflineMsgDone sends end of offline messages
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4)
- // seq2 should be the seq2 of the client's offline msg request, or 0 if unsolicited
- func (h *V4Handler) sendOfflineMsgDone(session *LegacySession, clientSeq2 uint16) error {
- pkt := make([]byte, 16)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvSysMsgDone)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], clientSeq2) // seq2 of offline msg request, or 0 if unsolicited
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- h.logger.Debug("sending V4 offline msg done",
- "uin", session.UIN,
- "seq2", clientSeq2,
- "hex", fmt.Sprintf("%X", pkt),
- )
- return h.sender.SendToSession(session, pkt)
- }
- // sendUserStatus sends user status change notification
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // From server.html: User changed his status (command 0x01a4)
- func (h *V4Handler) sendUserStatus(session *LegacySession, uin uint32, status uint32) error {
- pkt := make([]byte, 24)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvUserStatus)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], 0)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- // User status data
- binary.LittleEndian.PutUint32(pkt[16:20], uin)
- binary.LittleEndian.PutUint32(pkt[20:24], status)
- h.logger.Debug("sending V4 user status",
- "uin", session.UIN,
- "target_uin", uin,
- "status", fmt.Sprintf("0x%08X", status),
- "hex", fmt.Sprintf("%X", pkt),
- )
- return h.sender.SendToSession(session, pkt)
- }
- // sendOnlineMessage sends an online system message to a user
- // V3 server packet format with checkcode
- // Format: header(16) + FROM_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
- func (h *V4Handler) sendOnlineMessage(session *LegacySession, fromUIN uint32, msgType uint16, message string, seq2 uint16) error {
- // Calculate packet size
- msgBytes := []byte(message)
- 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
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[offset:], 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("V4 sending online message",
- "to", session.UIN,
- "from", fromUIN,
- "type", fmt.Sprintf("0x%04X", msgType),
- "msg_len", len(msgBytes),
- )
- return h.sender.SendToSession(session, pkt[:offset])
- }
- // sendUserOffline sends user offline notification with V4 checkcode
- // V3 server packet format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + CHECKCODE(4) + DATA
- // Data: OFFLINE_UIN(4)
- func (h *V4Handler) sendUserOffline(session *LegacySession, uin uint32) error {
- pkt := make([]byte, 20)
- binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
- binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvUserOffline)
- binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
- binary.LittleEndian.PutUint16(pkt[6:8], 0)
- binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
- // Calculate and add checkcode
- checkcode := h.calculateV4Checkcode(pkt)
- binary.LittleEndian.PutUint32(pkt[12:16], checkcode)
- // UIN of the user who went offline
- binary.LittleEndian.PutUint32(pkt[16:20], uin)
- h.logger.Debug("sending V4 user offline notification",
- "to", session.UIN,
- "offline_uin", uin,
- )
- return h.sender.SendToSession(session, pkt)
- }
|