Skip to content

Commit

Permalink
fix crash at login in AIM 1.7.563-1.1.8.924
Browse files Browse the repository at this point in the history
Fields "LastTime" and "CurrentState" on SNAC(0x01,0x07) cause AIM 1.x
to crash at login. The commit omits those fields when AIM 1.x clients
connect.

AIM 1.7.486 and under still crash for different reasons.
  • Loading branch information
mk6i committed Oct 31, 2024
1 parent 1762cf2 commit 9665333
Show file tree
Hide file tree
Showing 11 changed files with 1,302 additions and 1,059 deletions.
149 changes: 104 additions & 45 deletions foodgroup/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ func (s AuthService) RegisterChatSession(authCookie []byte) (*state.Session, err
return s.chatSessionRegistry.AddSession(c.ChatCookie, c.ScreenName), nil
}

// bosCookie represents a token containing client metadata passed to the BOS
// service upon connection.
type bosCookie struct {
ScreenName state.DisplayScreenName `oscar:"len_prefix=uint8"`
ClientID string `oscar:"len_prefix=uint8"`
}

// RegisterBOSSession adds a new session to the session registry.
Expand Down Expand Up @@ -108,6 +111,9 @@ func (s AuthService) RegisterBOSSession(authCookie []byte) (*state.Session, erro
sess.SetUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
}

// set string containing OSCAR client name and version
sess.SetClientID(c.ClientID)

if u.DisplayScreenName.IsUIN() {
sess.SetUserInfoFlag(wire.OServiceUserFlagICQ)

Expand Down Expand Up @@ -229,7 +235,10 @@ func (s AuthService) BUCPChallenge(
// (wire.LoginTLVTagsReconnectHere) and an authorization cookie
// (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
// (wire.LoginTLVTagsErrorSubcode).
func (s AuthService) BUCPLogin(bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.SNACMessage, error) {
func (s AuthService) BUCPLogin(
bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest,
newUserFn func(screenName state.DisplayScreenName) (state.User, error),
) (wire.SNACMessage, error) {

block, err := s.login(bodyIn.TLVList, newUserFn)
if err != nil {
Expand Down Expand Up @@ -257,82 +266,132 @@ func (s AuthService) BUCPLogin(bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newU
// (wire.LoginTLVTagsReconnectHere) and an authorization cookie
// (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
// (wire.LoginTLVTagsErrorSubcode).
func (s AuthService) FLAPLogin(frame wire.FLAPSignonFrame, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.TLVRestBlock, error) {
func (s AuthService) FLAPLogin(
frame wire.FLAPSignonFrame,
newUserFn func(screenName state.DisplayScreenName) (state.User, error),
) (wire.TLVRestBlock, error) {
return s.login(frame.TLVList, newUserFn)
}

// loginProperties represents the properties sent by the client at login.
type loginProperties struct {
screenName state.DisplayScreenName
clientID string
isBUCPAuth bool
passwordHash []byte
roastedPass []byte
}

// fromTLV creates an instance of loginProperties from a TLV list.
func (l *loginProperties) fromTLV(list wire.TLVList) error {
// extract screen name
if screenName, found := list.String(wire.LoginTLVTagsScreenName); found {
l.screenName = state.DisplayScreenName(screenName)
} else {
return errors.New("screen name doesn't exist in tlv")
}

// extract client name and version
if clientID, found := list.String(wire.LoginTLVTagsClientIdentity); found {
l.clientID = clientID
}

// get the password from the appropriate TLV. older clients have a
// roasted password, newer clients have a hashed password. ICQ may omit
// the password TLV when logging in without saved password.

// extract password hash for BUCP login
if passwordHash, found := list.Bytes(wire.LoginTLVTagsPasswordHash); found {
l.passwordHash = passwordHash
l.isBUCPAuth = true
}

// extract roasted password for FLAP login
if roastedPass, found := list.Bytes(wire.LoginTLVTagsRoastedPassword); found {
l.roastedPass = roastedPass
}

return nil
}

// login validates a user's credentials and creates their session. it returns
// metadata used in both BUCP and FLAP authentication responses.
func (s AuthService) login(
TLVList wire.TLVList,
tlv wire.TLVList,
newUserFn func(screenName state.DisplayScreenName) (state.User, error),
) (wire.TLVRestBlock, error) {

screenName, found := TLVList.String(wire.LoginTLVTagsScreenName)
if !found {
return wire.TLVRestBlock{}, errors.New("screen name doesn't exist in tlv")
props := loginProperties{}
if err := props.fromTLV(tlv); err != nil {
return wire.TLVRestBlock{}, err
}

sn := state.DisplayScreenName(screenName)

user, err := s.userManager.User(sn.IdentScreenName())
user, err := s.userManager.User(props.screenName.IdentScreenName())
if err != nil {
return wire.TLVRestBlock{}, err
}

if user == nil {
// user not found
if s.config.DisableAuth {
handleValid := false
if sn.IsUIN() {
handleValid = sn.ValidateUIN() == nil
} else {
handleValid = sn.ValidateAIMHandle() == nil
}
if !handleValid {
return loginFailureResponse(sn, wire.LoginErrInvalidUsernameOrPassword), nil
}

newUser, err := newUserFn(sn)
if err != nil {
// auth disabled, create the user and return success
if err := s.createUser(props, newUserFn); err != nil {
return wire.TLVRestBlock{}, err
}
if err := s.userManager.InsertUser(newUser); err != nil {
return wire.TLVRestBlock{}, err
}

return s.loginSuccessResponse(sn, err)
return s.loginSuccessResponse(props)
}

// auth enabled, return separate login errors for ICQ and AIM
loginErr := wire.LoginErrInvalidUsernameOrPassword
if sn.IsUIN() {
if props.screenName.IsUIN() {
loginErr = wire.LoginErrICQUserErr
}
return loginFailureResponse(sn, loginErr), nil
return loginFailureResponse(props, loginErr), nil
}

if s.config.DisableAuth {
return s.loginSuccessResponse(sn, err)
// user exists, but don't validate
return s.loginSuccessResponse(props)
}

var loginOK bool
// get the password from the appropriate TLV. older clients have a
// roasted password, newer clients have a hashed password. ICQ may omit
// the password TLV when logging in without saved password.
if md5Hash, hasMD5 := TLVList.Bytes(wire.LoginTLVTagsPasswordHash); hasMD5 {
loginOK = user.ValidateHash(md5Hash)
} else if roastedPass, hasRoasted := TLVList.Bytes(wire.LoginTLVTagsRoastedPassword); hasRoasted {
loginOK = user.ValidateRoastedPass(roastedPass)
if props.isBUCPAuth {
loginOK = user.ValidateHash(props.passwordHash)
} else {
loginOK = user.ValidateRoastedPass(props.roastedPass)
}
if !loginOK {
return loginFailureResponse(sn, wire.LoginErrInvalidPassword), nil
return loginFailureResponse(props, wire.LoginErrInvalidPassword), nil
}

return s.loginSuccessResponse(sn, err)
return s.loginSuccessResponse(props)
}

func (s AuthService) createUser(
props loginProperties,
newUserFn func(screenName state.DisplayScreenName) (state.User, error),
) error {

handleValid := false
if props.screenName.IsUIN() {
handleValid = props.screenName.ValidateUIN() == nil
} else {
handleValid = props.screenName.ValidateAIMHandle() == nil
}
if !handleValid {
return nil
}

newUser, err := newUserFn(props.screenName)
if err != nil {
return err
}
return s.userManager.InsertUser(newUser)
}

func (s AuthService) loginSuccessResponse(screenName state.DisplayScreenName, err error) (wire.TLVRestBlock, error) {
func (s AuthService) loginSuccessResponse(props loginProperties) (wire.TLVRestBlock, error) {
loginCookie := bosCookie{
ScreenName: screenName,
ScreenName: props.screenName,
ClientID: props.clientID,
}

buf := &bytes.Buffer{}
Expand All @@ -346,18 +405,18 @@ func (s AuthService) loginSuccessResponse(screenName state.DisplayScreenName, er

return wire.TLVRestBlock{
TLVList: []wire.TLV{
wire.NewTLVBE(wire.LoginTLVTagsScreenName, screenName),
wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, net.JoinHostPort(s.config.OSCARHost, s.config.BOSPort)),
wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, cookie),
},
}, nil
}

func loginFailureResponse(screenName state.DisplayScreenName, code uint16) wire.TLVRestBlock {
func loginFailureResponse(props loginProperties, errCode uint16) wire.TLVRestBlock {
return wire.TLVRestBlock{
TLVList: []wire.TLV{
wire.NewTLVBE(wire.LoginTLVTagsScreenName, screenName),
wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, code),
wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, errCode),
},
}
}
2 changes: 2 additions & 0 deletions foodgroup/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
dataIn: func() []byte {
loginCookie := bosCookie{
ScreenName: user.DisplayScreenName,
ClientID: "ICQ 2000b",
}
buf := &bytes.Buffer{}
assert.NoError(t, wire.MarshalBE(loginCookie, buf))
Expand Down Expand Up @@ -554,6 +555,7 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
dataIn: func() []byte {
loginCookie := bosCookie{
ScreenName: user.DisplayScreenName,
ClientID: "ICQ 2000b",
}
buf := &bytes.Buffer{}
assert.NoError(t, wire.MarshalBE(loginCookie, buf))
Expand Down
84 changes: 76 additions & 8 deletions foodgroup/oservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"net"
"strings"
"time"

"github.com/mk6i/retro-aim-server/config"
Expand Down Expand Up @@ -44,7 +45,9 @@ func (s OServiceService) ClientVersions(_ context.Context, frame wire.SNACFrame,
}
}

var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
// rateLimitSNACV1 is the rate params reply sent to AIM 1.x clients that does
// not contain LastTime and CurrentState fields.
var rateLimitSNACV1 = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
RateClasses: []struct {
ID uint16
WindowSize uint32
Expand All @@ -54,8 +57,10 @@ var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
DisconnectLevel uint32
CurrentLevel uint32
MaxLevel uint32
LastTime uint32
CurrentState uint8
V2Params *struct {
LastTime uint32
CurrentState uint8
} `oscar:"optional"`
}{
{
ID: 0x01,
Expand All @@ -66,8 +71,58 @@ var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
DisconnectLevel: 0x0320,
CurrentLevel: 0x0D69,
MaxLevel: 0x1770,
LastTime: 0x0000,
CurrentState: 0x0,
V2Params: nil,
},
},
RateGroups: []struct {
ID uint16
Pairs []struct {
FoodGroup uint16
SubGroup uint16
} `oscar:"count_prefix=uint16"`
}{
{
ID: 1,
Pairs: []struct {
FoodGroup uint16
SubGroup uint16
}{},
},
},
}

// rateLimitSNACV2 is the rate params reply sent to non-AIM 1.x clients.
var rateLimitSNACV2 = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
RateClasses: []struct {
ID uint16
WindowSize uint32
ClearLevel uint32
AlertLevel uint32
LimitLevel uint32
DisconnectLevel uint32
CurrentLevel uint32
MaxLevel uint32
V2Params *struct {
LastTime uint32
CurrentState uint8
} `oscar:"optional"`
}{
{
ID: 0x01,
WindowSize: 0x0050,
ClearLevel: 0x09C4,
AlertLevel: 0x07D0,
LimitLevel: 0x05DC,
DisconnectLevel: 0x0320,
CurrentLevel: 0x0D69,
MaxLevel: 0x1770,
V2Params: &struct {
LastTime uint32
CurrentState uint8
}{
LastTime: 0x0000,
CurrentState: 0x0,
},
},
},
RateGroups: []struct {
Expand Down Expand Up @@ -374,7 +429,16 @@ func init() {
} {
subGroups := foodGroupToSubgroup[foodGroup]
for _, subGroup := range subGroups {
rateLimitSNAC.RateGroups[0].Pairs = append(rateLimitSNAC.RateGroups[0].Pairs, struct {
// build response for AIM 1.x clients
rateLimitSNACV1.RateGroups[0].Pairs = append(rateLimitSNACV1.RateGroups[0].Pairs, struct {
FoodGroup uint16
SubGroup uint16
}{
FoodGroup: foodGroup,
SubGroup: subGroup,
})
// build response for all other clients
rateLimitSNACV2.RateGroups[0].Pairs = append(rateLimitSNACV2.RateGroups[0].Pairs, struct {
FoodGroup uint16
SubGroup uint16
}{
Expand Down Expand Up @@ -404,14 +468,18 @@ func init() {
// AIM clients silently fail when they expect a rate limit rule that does not
// exist in this response. When support for a new food group is added to the
// server, update this function accordingly.
func (s OServiceService) RateParamsQuery(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
func (s OServiceService) RateParamsQuery(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame) wire.SNACMessage {
limits := rateLimitSNACV2
if strings.Contains(sess.ClientID(), "AOL Instant Messenger (TM), version 1.") {
limits = rateLimitSNACV1
}
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.OService,
SubGroup: wire.OServiceRateParamsReply,
RequestID: inFrame.RequestID,
},
Body: rateLimitSNAC,
Body: limits,
}
}

Expand Down
Loading

0 comments on commit 9665333

Please sign in to comment.