From b660db44628885be224750366a6671db6444ed0a Mon Sep 17 00:00:00 2001 From: Artem Titoulenko Date: Sun, 19 Dec 2021 17:17:47 -0500 Subject: [PATCH] Location Services --- 0x01_generic_service_controls.go | 15 +-- 0x02_location_services.go | 154 +++++++++++++++++++++++++++++++ 0x03_buddy_list_management.go | 2 +- aimerror/errors.go | 8 +- main.go | 10 +- models/User.go | 33 ++++--- models/fixtures.yml | 6 +- online_routine.go | 96 ++++++++----------- util/util.go | 4 + 9 files changed, 242 insertions(+), 86 deletions(-) create mode 100644 0x02_location_services.go diff --git a/0x01_generic_service_controls.go b/0x01_generic_service_controls.go index 660ada4..d0fc2bf 100644 --- a/0x01_generic_service_controls.go +++ b/0x01_generic_service_controls.go @@ -18,6 +18,7 @@ var versions map[uint16]uint16 func init() { versions = make(map[uint16]uint16) versions[1] = 3 + versions[2] = 1 versions[3] = 1 versions[4] = 1 versions[17] = 1 @@ -36,7 +37,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna case 0x02: user := models.UserFromContext(ctx) if user != nil { - user.Status = models.UserStatusActive + user.Status = models.UserStatusOnline if err := user.Update(ctx, db); err != nil { return ctx, errors.Wrap(err, "could not set user as active") } @@ -51,7 +52,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna } for _, buddy := range buddies { - if buddy.Source.Status != models.UserStatusActive { + if buddy.Source.Status != models.UserStatusOnline { continue } @@ -61,7 +62,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna tlvs := []*oscar.TLV{ oscar.NewTLV(1, util.Word(0)), // TODO: user class - oscar.NewTLV(0x06, util.Dword(0x50)), // TODO: User Status + oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time @@ -110,7 +111,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna // TODO: make actual rate groups instead of this hack. I can't tell which subtypes are supported so // make it set rate limits for everything family for all subtypes under 0x21. - rg.WriteUint16(3 * 0x21) // Number of rate groups + rg.WriteUint16(uint16(len(versions)) * 0x21) // Number of rate groups for family := range versions { for subtype := 0; subtype < 0x21; subtype++ { rg.WriteUint16(family) @@ -135,14 +136,14 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna onlineSnac.Data.WriteString(user.Username) onlineSnac.Data.WriteUint16(0) // warning level - user.Status = models.UserStatusActive + user.Status = models.UserStatusOnline if err := user.Update(ctx, db); err != nil { return ctx, errors.Wrap(err, "could not set user as active") } tlvs := []*oscar.TLV{ - oscar.NewTLV(0x01, util.Dword(0x80)), // User Class - oscar.NewTLV(0x06, util.Dword(0x50)), // TODO: User Status + oscar.NewTLV(0x01, util.Dword(0)), // User Class + oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time diff --git a/0x02_location_services.go b/0x02_location_services.go new file mode 100644 index 0000000..aa72049 --- /dev/null +++ b/0x02_location_services.go @@ -0,0 +1,154 @@ +package main + +import ( + "aim-oscar/aimerror" + "aim-oscar/models" + "aim-oscar/oscar" + "aim-oscar/util" + "context" + "time" + + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type LocationServices struct { + OnlineCh chan *models.User +} + +func (s *LocationServices) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) { + session, _ := oscar.SessionFromContext(ctx) + + switch snac.Header.Subtype { + + // Client wants to know the limits/permissions for Location services + case 0x02: + paramsSnac := oscar.NewSNAC(2, 3) + paramsSnac.Data.WriteBinary(oscar.NewTLV(1, util.Word(256))) // Max profile length TODO: error if user sends more + + paramsFlap := oscar.NewFLAP(2) + paramsFlap.Data.WriteBinary(paramsSnac) + + return ctx, session.Send(paramsFlap) + + // Client set profile/away message + case 0x04: + user := models.UserFromContext(ctx) + if user == nil { + return ctx, aimerror.NoUserInSession + } + + tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes()) + if err != nil { + return nil, errors.Wrap(err, "authentication request missing TLVs") + } + + awayMessageTLV := oscar.FindTLV(tlvs, 0x4) + if awayMessageTLV != nil { + // Away message encoding is set in TLV 0x3 + awayMessageMimeTLV := oscar.FindTLV(tlvs, 0x3) + if awayMessageMimeTLV == nil { + return nil, errors.New("missing away message mime TLV 0x3") + } + user.AwayMessage = string(awayMessageTLV.Data) + user.AwayMessageEncoding = string(awayMessageMimeTLV.Data) + + } + + profileTLV := oscar.FindTLV(tlvs, 0x2) + if profileTLV != nil { + profileMimeTLV := oscar.FindTLV(tlvs, 0x1) + if profileMimeTLV == nil { + return nil, errors.New("missing away message mime TLV 0x3") + } + user.Profile = string(profileTLV.Data) + user.ProfileEncoding = string(profileMimeTLV.Data) + } + + if user.AwayMessage == "" { + user.Status = models.UserStatusOnline + } else { + user.Status = models.UserStatusAway + } + + if err := user.Update(ctx, db); err != nil { + return ctx, errors.Wrap(err, "could not set away message") + } + + s.OnlineCh <- user + + return models.NewContextWithUser(ctx, user), nil + + // Client is asking for user information like profile, away message, online state + case 0x5: + requestType, err := snac.Data.ReadUint16() + if err != nil { + return ctx, errors.Wrap(err, "missing request type") + } + + requestedUsername, err := snac.Data.ReadLPString() + if err != nil { + return ctx, errors.Wrap(err, "missing requested username") + } + + requestedUser, err := models.UserByUsername(ctx, db, requestedUsername) + if err != nil { + return ctx, aimerror.FetchingUser(err, requestedUsername) + } + + respSnac := oscar.NewSNAC(2, 6) + respSnac.Data.WriteLPString(requestedUser.Username) + respSnac.Data.WriteUint16(0) // TODO: warning level + + tlvs := []*oscar.TLV{ + oscar.NewTLV(1, util.Dword(0x80)), // user class + // oscar.NewTLV(6, util.Dword(uint32(requestedUser.Status))), // user status + // oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // user external IP + oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(requestedUser.LastActivityAt).Seconds()))), // idle time + oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // TODO: signon time + // oscar.NewTLV(0x05, util.Dword(uint32(requestedUser.CreatedAt.Unix()))), // member since + } + + respSnac.Data.WriteUint16(uint16(len(tlvs))) // number of TLVs + for _, tlv := range tlvs { + respSnac.Data.WriteBinary(tlv) + } + + // General info (Profile) + if requestType == 1 { + respSnac.Data.WriteBinary(oscar.NewTLV(1, util.LPString(requestedUser.ProfileEncoding))) + respSnac.Data.WriteBinary(oscar.NewTLV(2, util.LPString(requestedUser.Profile))) + } + + // Request Type 2 = online status, no TLVs + + // Away message + if requestType == 3 { + respSnac.Data.WriteBinary(oscar.NewTLV(3, util.LPString(requestedUser.AwayMessageEncoding))) + respSnac.Data.WriteBinary(oscar.NewTLV(4, util.LPString(requestedUser.AwayMessage))) + } + + // TODO: Request Type 4 - User capabilities + + respFlap := oscar.NewFLAP(2) + respFlap.Data.WriteBinary(respSnac) + + return ctx, session.Send(respFlap) + + case 0xb: + /* Nobody seems to know what this client request is for + - http://iserverd.khstu.ru/oscar/snac_02_0b.html + - https://bugs.bitlbee.org/browser/protocols/oscar/info.c?rev=b7d3cc34f68dab7b8f7d0777711317b334fc2219#L572 + + But the one dump that exists looks like a TLV 0x1 with empty data + */ + unknownSnac := oscar.NewSNAC(2, 0xc) + unknownSnac.Data.WriteUint16(1) + unknownSnac.Data.WriteUint16(0) + unknownFlap := oscar.NewFLAP(2) + unknownFlap.Data.WriteBinary(unknownSnac) + return ctx, session.Send(unknownFlap) + } + + return ctx, nil +} diff --git a/0x03_buddy_list_management.go b/0x03_buddy_list_management.go index c411f66..9353503 100644 --- a/0x03_buddy_list_management.go +++ b/0x03_buddy_list_management.go @@ -87,7 +87,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac * return ctx, aimerror.UserNotFound(buddyScreename) } - _, err = db.NewDelete().Model((*models.Buddy)(nil)).Where("uin = ?", user.UIN).Where("with_uin = ?", buddy.UIN).Exec(ctx) + _, err = db.NewDelete().Model((*models.Buddy)(nil)).Where("source_uin = ?", user.UIN).Where("with_uin = ?", buddy.UIN).Exec(ctx) if err != nil { return ctx, err } diff --git a/aimerror/errors.go b/aimerror/errors.go index 1cdfe71..9a7c0f9 100644 --- a/aimerror/errors.go +++ b/aimerror/errors.go @@ -2,8 +2,12 @@ package aimerror import "github.com/pkg/errors" -func UserNotFound(screenname string) error { - return errors.Errorf("no user with UIN %s", screenname) +func FetchingUser(err error, username string) error { + return errors.Wrapf(err, "could not fetch user with username %s", username) +} + +func UserNotFound(username string) error { + return errors.Errorf("no user with UIN %s", username) } var NoUserInSession = errors.New("no user in session") diff --git a/main.go b/main.go index 064ad05..d61a829 100644 --- a/main.go +++ b/main.go @@ -99,12 +99,15 @@ func main() { user := models.UserFromContext(ctx) if user != nil { - user.Status = models.UserStatusInactive + // tellBuddies := user.Status != models.UserStatusAway + user.Status = models.UserStatusAway if err := user.Update(ctx, db); err != nil { log.Print(errors.Wrap(err, "could not set user as inactive")) } - onlineCh <- user + if true { + onlineCh <- user + } } } @@ -176,10 +179,11 @@ func main() { handler := oscar.NewHandler(handleFn, handleCloseFn) - RegisterService(0x17, &AuthorizationRegistrationService{}) RegisterService(0x01, &GenericServiceControls{OnlineCh: onlineCh}) + RegisterService(0x02, &LocationServices{OnlineCh: onlineCh}) RegisterService(0x03, &BuddyListManagement{}) RegisterService(0x04, &ICBM{CommCh: commCh}) + RegisterService(0x17, &AuthorizationRegistrationService{}) exitChan := make(chan os.Signal, 1) signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT) diff --git a/models/User.go b/models/User.go index bcd2795..73f6e85 100644 --- a/models/User.go +++ b/models/User.go @@ -12,21 +12,30 @@ import ( type UserStatus int16 const ( - UserStatusInactive UserStatus = iota - UserStatusActive + UserStatusOnline = 0 + UserStatusAway = 1 + UserStatusDnd = 2 + UserStatusNA = 4 + UserStatusOccupied = 0x10 + UserStatusFree4Chat = 0x20 + UserStatusInvisible = 0x100 ) type User struct { - bun.BaseModel `bun:"table:users"` - UIN int64 `bun:",pk,autoincrement"` - Email string `bun:",unique"` - Username string `bun:",unique"` - Password string - Cipher string - CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - Status UserStatus - LastActivityAt time.Time `bin:"-"` + bun.BaseModel `bun:"table:users"` + UIN int64 `bun:",pk,autoincrement"` + Email string `bun:",unique"` + Username string `bun:",unique"` + Password string + Cipher string + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + Status UserStatus + Profile string + ProfileEncoding string + AwayMessage string + AwayMessageEncoding string + LastActivityAt time.Time `bin:"-"` } type userKey string diff --git a/models/fixtures.yml b/models/fixtures.yml index 88646c2..d4c679f 100644 --- a/models/fixtures.yml +++ b/models/fixtures.yml @@ -11,8 +11,4 @@ - model: Message rows: [] - model: Buddy - rows: - - source_uin: 1 - with_uin: 2 - - source_uin: 2 - with_uin: 1 + rows: [] diff --git a/online_routine.go b/online_routine.go index e839882..94bb36a 100644 --- a/online_routine.go +++ b/online_routine.go @@ -6,7 +6,6 @@ import ( "aim-oscar/util" "context" "encoding/binary" - "fmt" "log" "time" @@ -26,66 +25,51 @@ func OnlineNotification() (chan *models.User, routineFn) { return } - if user.Status == models.UserStatusActive { - fmt.Printf("%s is online", user.Username) - - var buddies []*models.Buddy - err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Relation("Target").Scan(context.Background(), &buddies) - if err != nil { - log.Printf("could not find user's buddies: %s", err.Error()) - return - } - - for _, buddy := range buddies { - if s := getSession(buddy.Source.Username); s != nil { - onlineSnac := oscar.NewSNAC(3, 0xb) - onlineSnac.Data.WriteLPString(user.Username) - onlineSnac.Data.WriteUint16(0) // TODO: user warning level - - tlvs := []*oscar.TLV{ - oscar.NewTLV(1, util.Word(0)), // TODO: user class - oscar.NewTLV(0x06, util.Dword(0x50)), // TODO: User Status - oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP - oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time - oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time - oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since - } - - onlineSnac.Data.WriteUint16(uint16(len(tlvs))) - for _, tlv := range tlvs { - onlineSnac.Data.WriteBinary(tlv) - } - - onlineFlap := oscar.NewFLAP(2) - onlineFlap.Data.WriteBinary(onlineSnac) - if err := s.Send(onlineFlap); err != nil { - log.Printf("could not tell %s that %s is online", buddy.Source.Username, buddy.Target.Username) - } - } - } + if user.Status == models.UserStatusOnline { + log.Printf("%s is now online", user.Username) + } else if user.Status == models.UserStatusAway { + log.Printf("%s is now away", user.Username) } - if user.Status == models.UserStatusInactive { - var buddies []*models.Buddy - err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Relation("Target").Scan(context.Background(), &buddies) - if err != nil { - log.Printf("could not find user's buddies: %s", err.Error()) - return + ctx := context.Background() + + // Find buddies who are friends with the user + var buddies []*models.Buddy + err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Scan(ctx, &buddies) + if err != nil { + log.Printf("could not find user's buddies: %s", err.Error()) + return + } + + for _, buddy := range buddies { + if buddy.Source.Status == models.UserStatusAway || buddy.Source.Status == models.UserStatusDnd { + continue } + log.Printf("telling %s that %s has a new status: %d!", buddy.Source.Username, user.Username, user.Status) - for _, buddy := range buddies { - if s := getSession(buddy.Source.Username); s != nil { - offlineSnac := oscar.NewSNAC(3, 0xb) - offlineSnac.Data.WriteLPString(user.Username) - offlineSnac.Data.WriteUint16(0) // TODO: user warning level - offlineSnac.Data.WriteUint16(1) - offlineSnac.Data.WriteBinary(oscar.NewTLV(1, util.Dword(0x80))) + if s := getSession(buddy.Source.Username); s != nil { + onlineSnac := oscar.NewSNAC(3, 0xb) + onlineSnac.Data.WriteLPString(user.Username) + onlineSnac.Data.WriteUint16(0) // TODO: user warning level - offlineFlap := oscar.NewFLAP(2) - offlineFlap.Data.WriteBinary(offlineSnac) - if err := s.Send(offlineFlap); err != nil { - log.Printf("could not tell %s that %s is offline", buddy.Source.Username, buddy.Target.Username) - } + tlvs := []*oscar.TLV{ + oscar.NewTLV(1, util.Word(0)), // TODO: user class + oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // TODO: User Status + oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP + oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time + oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time + oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since + } + + onlineSnac.Data.WriteUint16(uint16(len(tlvs))) + for _, tlv := range tlvs { + onlineSnac.Data.WriteBinary(tlv) + } + + onlineFlap := oscar.NewFLAP(2) + onlineFlap.Data.WriteBinary(onlineSnac) + if err := s.Send(onlineFlap); err != nil { + log.Printf("could not tell %s that %s is online", buddy.Source.Username, user.Username) } } } diff --git a/util/util.go b/util/util.go index 48d113a..441e077 100644 --- a/util/util.go +++ b/util/util.go @@ -60,3 +60,7 @@ func Word(x uint16) []byte { func Dword(x uint32) []byte { return []byte{byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x & 0xf)} } + +func LPString(x string) []byte { + return append(Word(uint16(len(x))), []byte(x)...) +}