| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- package icq_legacy
- import (
- "context"
- "log/slog"
- "net"
- "sync"
- "time"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // truncateField truncates a string field to maxLen if it exceeds the limit,
- // logging a warning when truncation occurs. This prevents oversized data from
- // crashing old ICQ clients that have fixed-size buffers.
- func truncateField(value string, maxLen int, logger *slog.Logger, fieldName string, uin uint32) string {
- if len(value) <= maxLen {
- return value
- }
- logger.Warn("truncating oversized field for legacy client",
- "uin", uin,
- "field", fieldName,
- "max", maxLen,
- "got", len(value),
- )
- return value[:maxLen]
- }
- // LegacySession represents a connected legacy ICQ client session.
- // It holds all state for a single UDP-based ICQ connection, including
- // protocol version, sequence numbers, contact/visibility lists, and
- // direct connection info for peer-to-peer messaging.
- type LegacySession struct {
- // UIN is the user's ICQ identification number.
- UIN uint32
- // Addr is the UDP address of the connected client.
- Addr *net.UDPAddr
- // Version is the ICQ protocol version (2, 3, 4, or 5).
- Version uint16
- // SessionID is the V5 session identifier used for packet encryption.
- SessionID uint32 // V5 only
- // SeqNumClient is the last received sequence number from the client.
- SeqNumClient uint16
- // SeqNumServer is the next sequence number to send to the client.
- SeqNumServer uint16
- // Status is the user's current online status (online, away, DND, etc.).
- Status uint32
- // LastActivity is the timestamp of the last received packet from this session.
- LastActivity time.Time
- // ContactList is the user's buddy list (UINs of contacts).
- ContactList []uint32
- // VisibleList contains UINs that can see this user when invisible.
- VisibleList []uint32
- // InvisibleList contains UINs that cannot see this user's online status.
- InvisibleList []uint32
- // Password is stored for session validation during the connection lifetime.
- Password string
- // Direct connection info (for peer-to-peer messaging)
- // TCPPort is the client's TCP listening port for direct connections.
- TCPPort uint32
- // InternalIP is the client's internal/LAN IP address for direct connections.
- InternalIP uint32
- // DCVersion is the client's direct connection protocol version.
- DCVersion uint16
- // DCType is the direct connection type (normal, SOCKS, etc.).
- DCType uint8
- // Instance links this legacy session to the unified OSCAR session manager.
- Instance *state.SessionInstance
- // knownOnline tracks contacts that have been reported as online via
- // SendUserOnline. Subsequent BuddyArrived for the same contact use
- // SendStatusChange instead, which sends the correct V5 packet type.
- knownOnline map[uint32]bool
- mu sync.RWMutex
- }
- // UpdateActivity updates the last activity timestamp
- func (s *LegacySession) UpdateActivity() {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.LastActivity = time.Now()
- }
- // GetLastActivity returns the last activity timestamp
- func (s *LegacySession) GetLastActivity() time.Time {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.LastActivity
- }
- // NextServerSeqNum returns and increments the server sequence number
- func (s *LegacySession) NextServerSeqNum() uint16 {
- s.mu.Lock()
- defer s.mu.Unlock()
- seq := s.SeqNumServer
- s.SeqNumServer++
- return seq
- }
- // SetStatus updates the session status
- func (s *LegacySession) SetStatus(status uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.Status = status
- }
- // GetStatus returns the current status
- func (s *LegacySession) GetStatus() uint32 {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.Status
- }
- // SetContactList updates the contact list
- func (s *LegacySession) SetContactList(contacts []uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.ContactList = make([]uint32, len(contacts))
- copy(s.ContactList, contacts)
- }
- // GetContactList returns a copy of the contact list
- func (s *LegacySession) GetContactList() []uint32 {
- s.mu.RLock()
- defer s.mu.RUnlock()
- contacts := make([]uint32, len(s.ContactList))
- copy(contacts, s.ContactList)
- return contacts
- }
- // SetVisibleList updates the visible list with a copy of the provided UINs.
- // Users on the visible list can see this session's online status even when invisible.
- func (s *LegacySession) SetVisibleList(visible []uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.VisibleList = make([]uint32, len(visible))
- copy(s.VisibleList, visible)
- }
- // SetInvisibleList updates the invisible list with a copy of the provided UINs.
- // Users on the invisible list cannot see this session's online status.
- func (s *LegacySession) SetInvisibleList(invisible []uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.InvisibleList = make([]uint32, len(invisible))
- copy(s.InvisibleList, invisible)
- }
- // IsOnVisibleList checks if a UIN is on the visible list
- func (s *LegacySession) IsOnVisibleList(uin uint32) bool {
- s.mu.RLock()
- defer s.mu.RUnlock()
- for _, v := range s.VisibleList {
- if v == uin {
- return true
- }
- }
- return false
- }
- // IsOnInvisibleList checks if a UIN is on the invisible list
- func (s *LegacySession) IsOnInvisibleList(uin uint32) bool {
- s.mu.RLock()
- defer s.mu.RUnlock()
- for _, v := range s.InvisibleList {
- if v == uin {
- return true
- }
- }
- return false
- }
- // IsContact checks if a UIN is in the contact list
- func (s *LegacySession) IsContact(uin uint32) bool {
- s.mu.RLock()
- defer s.mu.RUnlock()
- for _, c := range s.ContactList {
- if c == uin {
- return true
- }
- }
- return false
- }
- // MarkContactOnline marks a contact as known-online. Returns true if the
- // contact was already known online (i.e. this is a status change, not an
- // initial arrival).
- func (s *LegacySession) MarkContactOnline(uin uint32) bool {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.knownOnline == nil {
- s.knownOnline = make(map[uint32]bool)
- }
- wasOnline := s.knownOnline[uin]
- s.knownOnline[uin] = true
- return wasOnline
- }
- // MarkContactOffline removes a contact from the known-online set.
- func (s *LegacySession) MarkContactOffline(uin uint32) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.knownOnline != nil {
- delete(s.knownOnline, uin)
- }
- }
- // GetUIN returns the session's UIN (implements LegacySessionInstance)
- func (s *LegacySession) GetUIN() uint32 {
- return s.UIN
- }
- // GetExternalIP returns the external IP as uint32 (from UDP address)
- func (s *LegacySession) GetExternalIP() uint32 {
- if s.Addr == nil || s.Addr.IP == nil {
- return 0
- }
- ip := s.Addr.IP.To4()
- if ip == nil {
- return 0
- }
- // ICQ stores IP in little-endian format
- return uint32(ip[0]) | uint32(ip[1])<<8 | uint32(ip[2])<<16 | uint32(ip[3])<<24
- }
- // GetTCPPort returns the TCP port for direct connections
- func (s *LegacySession) GetTCPPort() uint32 {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.TCPPort
- }
- // GetInternalIP returns the internal IP for direct connections
- func (s *LegacySession) GetInternalIP() uint32 {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.InternalIP
- }
- // GetDCVersion returns the direct connection protocol version
- func (s *LegacySession) GetDCVersion() uint16 {
- s.mu.RLock()
- defer s.mu.RUnlock()
- return s.DCVersion
- }
- // SetDirectConnectionInfo sets the direct connection parameters
- func (s *LegacySession) SetDirectConnectionInfo(tcpPort, internalIP uint32, dcVersion uint16, dcType uint8) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.TCPPort = tcpPort
- s.InternalIP = internalIP
- s.DCVersion = dcVersion
- s.DCType = dcType
- }
- // UserManager provides user lookup and management for legacy ICQ operations.
- type UserManager interface {
- User(ctx context.Context, screenName state.IdentScreenName) (*state.User, error)
- InsertUser(ctx context.Context, u state.User) error
- DeleteUser(ctx context.Context, screenName state.IdentScreenName) error
- }
- // AccountManager provides password management for legacy ICQ operations.
- type AccountManager interface {
- SetUserPassword(ctx context.Context, screenName state.IdentScreenName, newPassword string) error
- }
- // SessionRetriever provides OSCAR session lookup for cross-protocol routing.
- type SessionRetriever interface {
- RetrieveSession(screenName state.IdentScreenName) *state.Session
- }
- // MessageRelayer provides SNAC message delivery for legacy→OSCAR routing.
- type MessageRelayer interface {
- RelayToScreenName(ctx context.Context, screenName state.IdentScreenName, msg wire.SNACMessage)
- }
- // BuddyBroadcaster provides presence broadcast for OSCAR buddy notifications.
- type BuddyBroadcaster interface {
- BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error
- BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error
- }
- // OfflineMessageManager provides offline message storage and retrieval.
- type OfflineMessageManager interface {
- DeleteMessages(ctx context.Context, recip state.IdentScreenName) error
- RetrieveMessages(ctx context.Context, recip state.IdentScreenName) ([]state.OfflineMessage, error)
- SaveMessage(ctx context.Context, offlineMessage state.OfflineMessage) (int, error)
- }
- // ICQUserFinder provides user search capabilities for legacy ICQ operations.
- type ICQUserFinder interface {
- FindByUIN(ctx context.Context, UIN uint32) (state.User, error)
- FindByICQEmail(ctx context.Context, email string) (state.User, error)
- FindByICQName(ctx context.Context, firstName, lastName, nickName string) ([]state.User, error)
- FindByICQInterests(ctx context.Context, code uint16, keywords []string) ([]state.User, error)
- FindByICQKeyword(ctx context.Context, keyword string) ([]state.User, error)
- SearchICQUsers(ctx context.Context, c state.ICQUserSearchCriteria) ([]state.User, error)
- }
- // ICQUserUpdater provides profile update capabilities for legacy ICQ operations.
- type ICQUserUpdater interface {
- SetBasicInfo(ctx context.Context, name state.IdentScreenName, data state.ICQBasicInfo) error
- SetWorkInfo(ctx context.Context, name state.IdentScreenName, data state.ICQWorkInfo) error
- SetMoreInfo(ctx context.Context, name state.IdentScreenName, data state.ICQMoreInfo) error
- SetInterests(ctx context.Context, name state.IdentScreenName, data state.ICQInterests) error
- SetAffiliations(ctx context.Context, name state.IdentScreenName, data state.ICQAffiliations) error
- SetUserNotes(ctx context.Context, name state.IdentScreenName, data state.ICQUserNotes) error
- SetPermissions(ctx context.Context, name state.IdentScreenName, data state.ICQPermissions) error
- SetHomepageCategory(ctx context.Context, name state.IdentScreenName, data state.ICQHomepageCategory) error
- SetICQInfo(ctx context.Context, name state.IdentScreenName, info state.ICQInfo) error
- }
- // FeedbagManager provides server-side buddy list access and modification.
- type FeedbagManager interface {
- Feedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
- FeedbagUpsert(ctx context.Context, screenName state.IdentScreenName, items []wire.FeedbagItem) error
- }
- // RelationshipFetcher provides buddy relationship lookup.
- type RelationshipFetcher interface {
- AllRelationships(ctx context.Context, me state.IdentScreenName, filter []state.IdentScreenName) ([]state.Relationship, error)
- Relationship(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) (state.Relationship, error)
- }
- // BuddyListRegistry provides buddy list registration for cross-protocol
- // presence visibility. Legacy sessions must register their buddy list so that
- // OSCAR's BroadcastVisibility/AllRelationships can discover them.
- type BuddyListRegistry interface {
- RegisterBuddyList(ctx context.Context, user state.IdentScreenName) error
- UnregisterBuddyList(ctx context.Context, user state.IdentScreenName) error
- }
- // BuddyService provides client-side buddy list management through the OSCAR
- // Buddy food group path used by OSCAR and TOC clients.
- type BuddyService interface {
- AddBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) (*wire.SNACMessage, error)
- }
- // LegacySessionInstance represents a legacy session as seen by the service layer.
- // This interface abstracts the session to avoid circular dependencies between
- // the foodgroup and server/icq_legacy packages.
- type LegacySessionInstance interface {
- // GetUIN returns the session's ICQ identification number.
- GetUIN() uint32
- // GetStatus returns the session's current online status value.
- GetStatus() uint32
- // GetContactList returns a copy of the session's contact list (buddy UINs).
- GetContactList() []uint32
- // IsOnVisibleList checks if the given UIN is on this session's visible list.
- IsOnVisibleList(uin uint32) bool
- // IsOnInvisibleList checks if the given UIN is on this session's invisible list.
- IsOnInvisibleList(uin uint32) bool
- }
- // LegacyMessageSender is the interface for sending messages to legacy ICQ clients.
- // It provides methods for delivering messages and status notifications to
- // connected legacy sessions.
- type LegacyMessageSender interface {
- // SendMessage delivers a message to a legacy client identified by UIN.
- SendMessage(uin uint32, fromUIN uint32, msgType uint16, message string) error
- // SendStatusUpdate sends a status change notification to a legacy client.
- SendStatusUpdate(uin uint32, targetUIN uint32, status uint32) error
- // SendUserOnline sends a user online notification to a legacy client.
- SendUserOnline(uin uint32, targetUIN uint32, status uint32, ip net.IP, port uint16) error
- // SendUserOffline sends a user offline notification to a legacy client.
- SendUserOffline(uin uint32, targetUIN uint32) error
- }
- // ICBMService is the interface for sending SNAC messages to the OSCAR ICBM service.
- type ICBMService interface {
- ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error)
- }
|