From 3e69ba3e4c8150078cbaa83370228a9897a4212c Mon Sep 17 00:00:00 2001 From: Artem Titoulenko Date: Sat, 18 Dec 2021 21:02:47 -0500 Subject: [PATCH] store messages sent through ICBM --- 0x01_generic_service_controls.go | 6 +- 0x04_ICBM.go | 105 +++++++++++++++++++++ 0x17_authorization_registration_service.go | 2 +- main.go | 2 +- models/Message.go | 33 +++++++ models/User.go | 4 +- models/fixtures.yml | 2 + oscar/buf.go | 79 ++++++++++++++++ oscar/buf_test.go | 55 +++++++++++ 9 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 models/Message.go create mode 100644 oscar/buf_test.go diff --git a/0x01_generic_service_controls.go b/0x01_generic_service_controls.go index e3b82b7..0d94469 100644 --- a/0x01_generic_service_controls.go +++ b/0x01_generic_service_controls.go @@ -6,7 +6,6 @@ import ( "aim-oscar/util" "context" "encoding/binary" - "fmt" "time" "github.com/pkg/errors" @@ -73,9 +72,8 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna } onlineSnac := oscar.NewSNAC(1, 0xf) - uin := fmt.Sprint(user.UIN) - onlineSnac.Data.WriteUint8(uint8(len(uin))) - onlineSnac.Data.WriteString(uin) + onlineSnac.Data.WriteUint8(uint8(len(user.Username))) + onlineSnac.Data.WriteString(user.Username) onlineSnac.Data.WriteUint16(0) // warning level user.Status = "active" diff --git a/0x04_ICBM.go b/0x04_ICBM.go index 7e0ce6d..a3b911d 100644 --- a/0x04_ICBM.go +++ b/0x04_ICBM.go @@ -1,10 +1,12 @@ package main import ( + "aim-oscar/models" "aim-oscar/oscar" "bytes" "context" "encoding/binary" + "log" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -85,6 +87,109 @@ func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) channelFlap.Data.WriteBinary(channelSnac) session.Send(channelFlap) + return ctx, nil + + // Client wants to send a message to someone through the server + case 0x06: + user := models.UserFromContext(ctx) + if user == nil { + return ctx, errors.New("context should have User") + } + + msgID, _ := snac.Data.ReadUint64() + msgChannel, _ := snac.Data.ReadUint16() + to, _ := snac.Data.ReadLPString() + + if msgChannel != 1 { + log.Printf("Message for unsupported channel %d", msgChannel) + return ctx, nil + } + + tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes()) + if err != nil { + return ctx, errors.Wrap(err, "could not unmarshal message tlvs") + } + + messageTLV := oscar.FindTLV(tlvs, 0x2) + if messageTLV == nil { + return ctx, errors.New("missing messageTLV 0x2") + } + + // Parse fragment (array of required capabilities, yawn) + messageTLVData := oscar.Buffer{} + messageTLVData.Write(messageTLV.Data) + + fragmentNum, err := messageTLVData.ReadUint8() + if err != nil { + return ctx, errors.Wrap(err, "could not read fragment identifier") + } else if fragmentNum != 5 { + return ctx, errors.New("expected first fragment identifier to be 5") + } + + fragmentVersion, err := messageTLVData.ReadUint8() + if err != nil { + return ctx, errors.Wrap(err, "could not read fragment version") + } else if fragmentVersion != 1 { + return ctx, errors.New("expected first fragment version to be 1") + } + + fragmentLength, err := messageTLVData.ReadUint16() + if err != nil { + return ctx, errors.Wrap(err, "could not read fragment data length") + } + + // Skip over all the capabilities + messageTLVData.Seek(int(fragmentLength)) + + // This should be the start of the message contents fragment + fragmentNum, err = messageTLVData.ReadUint8() + if err != nil { + return ctx, errors.Wrap(err, "could not read fragment identifier") + } else if fragmentNum != 1 { + return ctx, errors.New("expected second fragment identifier to be 1") + } + + fragmentVersion, err = messageTLVData.ReadUint8() + if err != nil { + return ctx, errors.Wrap(err, "could not read fragment version") + } else if fragmentVersion != 1 { + return ctx, errors.New("expected second fragment version to be 1") + } + + fragmentLength, err = messageTLVData.ReadUint16() + if err != nil { + return ctx, errors.Wrap(err, "could not read second fragment data length") + } + + // Skip over the charset + language + messageTLVData.Seek(4) + + messageContents := make([]byte, fragmentLength-4) + n, err := messageTLVData.Read(messageContents) + if err != nil { + return ctx, errors.Wrap(err, "could not read message contents from fragment") + } + if n < int(fragmentLength)-4 { + return ctx, errors.New("read insufficient data from message fragment") + } + + if err = models.InsertMessage(ctx, db, msgID, user.Username, to, string(messageContents)); err != nil { + return ctx, errors.Wrap(err, "could not insert message") + } + + // The Client usually wants a response that the server got the message. It checks that the message + // back has the same message ID that was sent and the user it was sent to. + ackTLV := oscar.FindTLV(tlvs, 3) + if ackTLV != nil { + ackSnac := oscar.NewSNAC(4, 0xc) + ackSnac.Data.WriteUint64(msgID) + ackSnac.Data.WriteUint16(2) + ackSnac.Data.WriteLPString(user.Username) + ackFlap := oscar.NewFLAP(2) + ackFlap.Data.WriteBinary(ackSnac) + return ctx, session.Send(ackFlap) + } + return ctx, nil } diff --git a/0x17_authorization_registration_service.go b/0x17_authorization_registration_service.go index 09bbdc8..b177315 100644 --- a/0x17_authorization_registration_service.go +++ b/0x17_authorization_registration_service.go @@ -22,7 +22,7 @@ const CIPHER_LENGTH = 64 const AIM_MD5_STRING = "AOL Instant Messenger (SM)" type AuthorizationCookie struct { - UIN int + UIN int64 X string } diff --git a/main.go b/main.go index 56757e9..6b973fc 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func main() { db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) // Register our DB models - db.RegisterModel((*models.User)(nil)) + db.RegisterModel((*models.User)(nil), (*models.Message)(nil)) // dev: load in fixtures to test against fixture := dbfixture.New(db, dbfixture.WithRecreateTables()) diff --git a/models/Message.go b/models/Message.go new file mode 100644 index 0000000..7c08d91 --- /dev/null +++ b/models/Message.go @@ -0,0 +1,33 @@ +package models + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type Message struct { + bun.BaseModel `bun:"table:messages"` + MessageID uint64 `bun:",pk,notnull,unique"` + From string + To string + Contents string + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + DeliveredAt time.Time `bun:",nullzero"` +} + +func InsertMessage(ctx context.Context, db *bun.DB, messageId uint64, from string, to string, contents string) error { + msg := &Message{ + MessageID: messageId, + From: from, + To: to, + Contents: contents, + } + if _, err := db.NewInsert().Model(msg).Exec(ctx); err != nil { + return errors.Wrap(err, "could not update user") + } + + return nil +} diff --git a/models/User.go b/models/User.go index 950d39f..a1ad05c 100644 --- a/models/User.go +++ b/models/User.go @@ -11,7 +11,7 @@ import ( type User struct { bun.BaseModel `bun:"table:users"` - UIN int `bun:",pk,autoincrement"` + UIN int64 `bun:",pk,autoincrement"` Email string `bun:",unique"` Username string `bun:",unique"` Password string @@ -43,7 +43,7 @@ func UserByUsername(ctx context.Context, db *bun.DB, username string) (*User, er return user, nil } -func UserByUIN(ctx context.Context, db *bun.DB, uin int) (*User, error) { +func UserByUIN(ctx context.Context, db *bun.DB, uin int64) (*User, error) { user := new(User) if err := db.NewSelect().Model(user).Where("uin = ?", uin).Scan(ctx, user); err != nil { if err == sql.ErrNoRows { diff --git a/models/fixtures.yml b/models/fixtures.yml index bfed850..5147a73 100644 --- a/models/fixtures.yml +++ b/models/fixtures.yml @@ -3,3 +3,5 @@ - username: toof password: bar email: toof@plot.club +- model: Message + rows: [] diff --git a/oscar/buf.go b/oscar/buf.go index c371651..eb5966a 100644 --- a/oscar/buf.go +++ b/oscar/buf.go @@ -4,12 +4,86 @@ import ( "aim-oscar/util" "encoding" "encoding/binary" + "io" ) +// Buffer is a handy byte slice that reads from the front and writes on the end type Buffer struct { d []byte } +// Seek moves the read cursor forward. If the cursor is beyond the length of the byte +// slice then the buffer slice is just replaced with an empty slice +func (b *Buffer) Seek(n int) { + if n > len(b.d) { + b.d = make([]byte, 0) + return + } + b.d = b.d[n:] +} + +func (b *Buffer) Read(d []byte) (int, error) { + if len(d) > len(b.d) { + return 0, io.EOF + } + + n := copy(d, b.d[0:len(d)]) + return n, nil +} + +func (b *Buffer) ReadUint8() (uint8, error) { + if len(b.d) < 1 { + return 0, io.EOF + } + ret := uint8(b.d[0]) + b.d = b.d[1:] + return ret, nil +} + +func (b *Buffer) ReadUint16() (uint16, error) { + if len(b.d) < 2 { + return 0, io.EOF + } + ret := binary.BigEndian.Uint16(b.d[0:2]) + b.d = b.d[2:] + return ret, nil +} + +func (b *Buffer) ReadUint32() (uint32, error) { + if len(b.d) < 4 { + return 0, io.EOF + } + ret := binary.BigEndian.Uint32(b.d[0:4]) + b.d = b.d[4:] + return ret, nil +} + +func (b *Buffer) ReadUint64() (uint64, error) { + if len(b.d) < 8 { + return 0, io.EOF + } + ret := binary.BigEndian.Uint64(b.d[0:8]) + b.d = b.d[8:] + return ret, nil +} + +// ReadLPString reads a length-prefixed string. The first byte should be the string length +// followed by that many bytes. Returns io.EOF if there are less bytes than indicated. +func (b *Buffer) ReadLPString() (string, error) { + length, err := b.ReadUint8() + if err != nil { + return "", nil + } + + if len(b.d) < int(length) { + return "", io.EOF + } + + str := string(b.d[:length]) + b.d = b.d[length:] + return str, nil +} + func (b *Buffer) WriteUint8(x uint8) { b.d = append(b.d, x) } @@ -33,6 +107,11 @@ func (b *Buffer) WriteString(x string) { b.d = append(b.d, []byte(x)...) } +func (b *Buffer) WriteLPString(x string) { + b.WriteUint8(uint8(len(x))) + b.WriteString(x) +} + func (b *Buffer) Write(x []byte) (int, error) { b.d = append(b.d, x...) return len(x), nil diff --git a/oscar/buf_test.go b/oscar/buf_test.go new file mode 100644 index 0000000..8dc9f89 --- /dev/null +++ b/oscar/buf_test.go @@ -0,0 +1,55 @@ +package oscar + +import "testing" + +func fail(t *testing.T, e error, method string) { + if e != nil { + t.Errorf("invalid read from %s: %s", method, e.Error()) + } +} + +func TestBuffer(t *testing.T) { + b := Buffer{} + + b.WriteUint8(uint8(1)) + b.WriteUint16(uint16(2)) + b.WriteUint32(uint32(3)) + b.WriteUint64(uint64(4)) + + x1, err := b.ReadUint8() + fail(t, err, "ReadUint8") + if x1 != 1 { + t.Errorf("expected ReadUint8 to read 1, got %d", x1) + } + + x2, err := b.ReadUint16() + fail(t, err, "ReadUint16") + if x2 != 2 { + t.Errorf("expected ReadUint16 to read 2, got %d", x2) + } + + x3, err := b.ReadUint32() + fail(t, err, "ReadUint32") + if x3 != 3 { + t.Errorf("expected ReadUint32 to read 3, got %d", x3) + } + + x4, err := b.ReadUint64() + fail(t, err, "ReadUint64") + if x4 != 4 { + t.Errorf("expected ReadUint64 to read 4, got %d", x4) + } +} + +func TestBufferLPString(t *testing.T) { + b := Buffer{} + + expectedStr := "This is a long string" + b.WriteLPString(expectedStr) + + str, err := b.ReadLPString() + fail(t, err, "ReadLPString") + if str != expectedStr { + t.Errorf("expected to read %s, got %s", expectedStr, str) + } +}