Location Services

This commit is contained in:
Artem Titoulenko 2021-12-19 17:17:47 -05:00
parent 9dc97738b3
commit b660db4462
9 changed files with 242 additions and 86 deletions

View file

@ -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

154
0x02_location_services.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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")

10
main.go
View file

@ -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)

View file

@ -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

View file

@ -11,8 +11,4 @@
- model: Message
rows: []
- model: Buddy
rows:
- source_uin: 1
with_uin: 2
- source_uin: 2
with_uin: 1
rows: []

View file

@ -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)
}
}
}

View file

@ -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)...)
}