diff --git a/0x01_generic_service_controls.go b/0x01_generic_service_controls.go new file mode 100644 index 0000000..fe63bb7 --- /dev/null +++ b/0x01_generic_service_controls.go @@ -0,0 +1,113 @@ +package main + +import ( + "aim-oscar/models" + "aim-oscar/oscar" + "aim-oscar/util" + "context" + "encoding/binary" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +var versions map[uint16]uint16 + +func init() { + versions = make(map[uint16]uint16) + versions[1] = 3 + versions[4] = 1 +} + +type GenericServiceControls struct{} + +func (g *GenericServiceControls) 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 rate limits for all services + case 0x06: + rateSnac := oscar.NewSNAC(1, 7) + rateSnac.Data.WriteUint16(1) // one rate class + + // Define a Rate Class + rc := oscar.Buffer{} + rc.WriteUint16(1) // ID + rc.WriteUint32(80) // Window Size + rc.WriteUint32(2500) // Clear level + rc.WriteUint32(2000) // Alert level + rc.WriteUint32(1500) // Limit level + rc.WriteUint32(800) // Disconnect level + rc.WriteUint32(3400) // Current level (fake) + rc.WriteUint32(6000) // Max level + rc.WriteUint32(0) // Last time ? + rc.WriteUint8(0) // Current state ? + rateSnac.Data.Write(rc.Bytes()) + + // Define a Rate Group + rg := oscar.Buffer{} + rg.WriteUint16(1) // ID + + // 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(2 * 0x21) // Number of rate groups + for family := range versions { + for subtype := 0; subtype < 0x21; subtype++ { + rg.WriteUint16(family) + rg.WriteUint16(uint16(subtype)) + } + } + rateSnac.Data.Write(rg.Bytes()) + + rateFlap := oscar.NewFLAP(2) + rateFlap.Data.WriteBinary(rateSnac) + return ctx, session.Send(rateFlap) + + // Client wants their own online information + case 0x0e: + user := models.UserFromContext(ctx) + if user == nil { + return ctx, errors.New("expecting user in context") + } + + onlineSnac := oscar.NewSNAC(1, 0xf) + uin := fmt.Sprint(user.UIN) + onlineSnac.Data.WriteUint8(uint8(len(uin))) + onlineSnac.Data.WriteString(uin) + onlineSnac.Data.WriteUint16(0) // warning level + + tlvs := []*oscar.TLV{ + oscar.NewTLV(0x01, util.Dword(0x80)), // User Class + oscar.NewTLV(0x06, util.Dword(0x0001|0x0100)), // User Status (TODO: update status in DB) + oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP + oscar.NewTLV(0x0f, util.Dword(0x0)), // Idle Time (TODO: track idle time) + oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time + oscar.NewTLV(0x01e, util.Dword(0x0)), // Unknown value + oscar.NewTLV(0x05, util.Dword(uint32(time.Now().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) + return ctx, session.Send(onlineFlap) + + // Client wants to know the versions of all of the services offered + case 0x17: + versionsSnac := oscar.NewSNAC(1, 0x18) + for family, version := range versions { + versionsSnac.Data.WriteUint16(family) + versionsSnac.Data.WriteUint16(version) + } + versionsFlap := oscar.NewFLAP(2) + versionsFlap.Data.WriteBinary(versionsSnac) + return ctx, session.Send(versionsFlap) + } + + return ctx, nil +} diff --git a/0x04_ICBM.go b/0x04_ICBM.go new file mode 100644 index 0000000..7e0ce6d --- /dev/null +++ b/0x04_ICBM.go @@ -0,0 +1,92 @@ +package main + +import ( + "aim-oscar/oscar" + "bytes" + "context" + "encoding/binary" + + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type ICBM struct{} + +type icbmKey string + +func (s icbmKey) String() string { + return "icbm-" + string(s) +} + +var ( + channelKey = icbmKey("channel") +) + +func NewContextWithChannel(ctx context.Context, c *channel) context.Context { + return context.WithValue(ctx, channelKey, c) +} + +func ChannelFromContext(ctx context.Context) *channel { + s := ctx.Value(channelKey) + if s == nil { + return nil + } + return s.(*channel) +} + +type channel struct { + ID uint16 + MessageFlags uint32 + MaxMessageSnacSize uint16 + MaxSenderWarningLevel uint16 + MaxReceiverWarningLevel uint16 + MinimumMessageInterval uint16 + Unknown uint16 +} + +func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) { + session, _ := oscar.SessionFromContext(ctx) + + switch snac.Header.Subtype { + // Client is telling us about their ICBM capabilities + case 0x02: + /* + xx xx word channel to setup + xx xx xx xx dword message flags + xx xx word max message snac size + xx xx word max sender warning level + xx xx word max receiver warning level + xx xx word minimum message interval (sec) + 00 00 word unknown parameter (also seen 03 E8) + */ + + channel := channel{} + r := bytes.NewReader(snac.Data.Bytes()) + if err := binary.Read(r, binary.BigEndian, &channel); err != nil { + return ctx, errors.Wrap(err, "could not read channel settings") + } + + newCtx := NewContextWithChannel(ctx, &channel) + return newCtx, nil + + // Client asks about the ICBM capabilities we set for them + case 0x04: + channel := ChannelFromContext(ctx) + channelSnac := oscar.NewSNAC(4, 5) + channelSnac.Data.WriteUint16(uint16(channel.ID)) + channelSnac.Data.WriteUint32(channel.MessageFlags) + channelSnac.Data.WriteUint16(channel.MaxMessageSnacSize) + channelSnac.Data.WriteUint16(channel.MaxSenderWarningLevel) + channelSnac.Data.WriteUint16(channel.MaxReceiverWarningLevel) + channelSnac.Data.WriteUint16(channel.MinimumMessageInterval) + channelSnac.Data.WriteUint16(channel.Unknown) + + channelFlap := oscar.NewFLAP(2) + channelFlap.Data.WriteBinary(channelSnac) + session.Send(channelFlap) + + return ctx, nil + } + + return ctx, nil +} diff --git a/0x17_authorization_registration_service.go b/0x17_authorization_registration_service.go index 281e26d..09bbdc8 100644 --- a/0x17_authorization_registration_service.go +++ b/0x17_authorization_registration_service.go @@ -21,7 +21,48 @@ import ( const CIPHER_LENGTH = 64 const AIM_MD5_STRING = "AOL Instant Messenger (SM)" -type AuthorizationRegistrationService struct { +type AuthorizationCookie struct { + UIN int + X string +} + +type AuthorizationRegistrationService struct{} + +func AuthenticateFLAPCookie(ctx context.Context, db *bun.DB, flap *oscar.FLAP) (*models.User, error) { + // Otherwise this is a protocol negotiation from the client. They're likely trying to connect + // and sending a cookie to verify who they are. + tlvs, err := oscar.UnmarshalTLVs(flap.Data.Bytes()[4:]) + if err != nil { + return nil, errors.Wrap(err, "authentication request missing TLVs") + } + + cookieTLV := oscar.FindTLV(tlvs, 0x6) + if cookieTLV == nil { + return nil, errors.New("authentication request missing Cookie TLV 0x6") + } + + auth := AuthorizationCookie{} + if err := json.Unmarshal(cookieTLV.Data, &auth); err != nil { + return nil, errors.Wrap(err, "could not unmarshal cookie") + } + + user, err := models.UserByUIN(ctx, db, auth.UIN) + if err != nil { + return nil, errors.Wrap(err, "could not get User by UIN") + } + + h := md5.New() + io.WriteString(h, user.Cipher) + io.WriteString(h, user.Password) + io.WriteString(h, AIM_MD5_STRING) + expectedPasswordHash := fmt.Sprintf("%x", h.Sum(nil)) + + // Make sure the hash passed in matches the one from the DB + if expectedPasswordHash != auth.X { + return nil, errors.New("unexpected cookie hash") + } + + return user, nil } func (a *AuthorizationRegistrationService) GenerateCipher() string { @@ -33,7 +74,12 @@ func (a *AuthorizationRegistrationService) GenerateCipher() string { return base32.StdEncoding.EncodeToString(randomBytes)[:CIPHER_LENGTH] } -func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar.Session, snac *oscar.SNAC) error { +func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) { + session, err := oscar.SessionFromContext(ctx) + if err != nil { + util.PanicIfError(err) + } + switch snac.Header.Subtype { // Request MD5 Auth Key case 0x06: @@ -42,14 +88,13 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar usernameTLV := oscar.FindTLV(tlvs, 1) if usernameTLV == nil { - return errors.New("missing username TLV") + return ctx, errors.New("missing username TLV") } // Fetch the user - ctx := context.Background() user, err := models.UserByUsername(ctx, db, string(usernameTLV.Data)) if err != nil { - return err + return ctx, err } if user == nil { snac := oscar.NewSNAC(0x17, 0x03) @@ -57,13 +102,13 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar snac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) resp := oscar.NewFLAP(2) resp.Data.WriteBinary(snac) - return session.Send(resp) + return ctx, session.Send(resp) } // Create cipher for this user user.Cipher = a.GenerateCipher() if err = user.Update(ctx, db); err != nil { - return err + return ctx, err } snac := oscar.NewSNAC(0x17, 0x07) @@ -72,7 +117,7 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar resp := oscar.NewFLAP(2) resp.Data.WriteBinary(snac) - return session.Send(resp) + return ctx, session.Send(resp) // Client Authorization Request case 0x02: @@ -81,14 +126,14 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar usernameTLV := oscar.FindTLV(tlvs, 1) if usernameTLV == nil { - return errors.New("missing username TLV 0x1") + return ctx, errors.New("missing username TLV 0x1") } username := string(usernameTLV.Data) ctx := context.Background() user, err := models.UserByUsername(ctx, db, username) if err != nil { - return err + return ctx, err } if user == nil { @@ -97,12 +142,12 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar snac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) resp := oscar.NewFLAP(2) resp.Data.WriteBinary(snac) - return session.Send(resp) + return ctx, session.Send(resp) } passwordHashTLV := oscar.FindTLV(tlvs, 0x25) if passwordHashTLV == nil { - return errors.New("missing password hash TLV 0x25") + return ctx, errors.New("missing password hash TLV 0x25") } // Compute password has that we expect the client to send back if the password was right @@ -123,7 +168,7 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar // Tell them to leave discoFlap := oscar.NewFLAP(4) - return session.Send(discoFlap) + return ctx, session.Send(discoFlap) } // Send BOS response + cookie @@ -131,10 +176,7 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar authSnac.Data.WriteBinary(usernameTLV) authSnac.Data.WriteBinary(oscar.NewTLV(0x5, []byte(SRV_ADDRESS))) - cookie, err := json.Marshal(struct { - UIN int - X string - }{ + cookie, err := json.Marshal(AuthorizationCookie{ UIN: user.UIN, X: fmt.Sprintf("%x", expectedPasswordHash), }) @@ -148,8 +190,9 @@ func (a *AuthorizationRegistrationService) HandleSNAC(db *bun.DB, session *oscar // Tell them to leave discoFlap := oscar.NewFLAP(4) - return session.Send(discoFlap) + session.Send(discoFlap) + return ctx, session.Disconnect() } - return nil + return ctx, nil } diff --git a/main.go b/main.go index 8b96101..f18f457 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( ) const ( - SRV_HOST = "" + SRV_HOST = "10.0.1.2" SRV_PORT = "5190" SRV_ADDRESS = SRV_HOST + ":" + SRV_PORT ) @@ -68,12 +68,40 @@ func main() { } defer listener.Close() - handler := oscar.NewHandler(func(session *oscar.Session, flap *oscar.FLAP) { + handler := oscar.NewHandler(func(ctx context.Context, flap *oscar.FLAP) context.Context { + session, err := oscar.SessionFromContext(ctx) + if err != nil { + util.PanicIfError(err) + } + + if user := models.UserFromContext(ctx); user != nil { + fmt.Printf("%s (%v) ->\n%+v\n", user.Username, session.RemoteAddr(), flap) + } else { + fmt.Printf("%v ->\n%+v\n", session.RemoteAddr(), flap) + } + if flap.Header.Channel == 1 { // Is this a hello? if bytes.Equal(flap.Data.Bytes(), []byte{0, 0, 0, 1}) { - return + return ctx } + + user, err := AuthenticateFLAPCookie(ctx, db, flap) + if err != nil { + log.Printf("Could not authenticate cookie: %s", err) + return ctx + } + ctx = models.NewContextWithUser(ctx, user) + + // Send available services + servicesSnac := oscar.NewSNAC(1, 3) + servicesSnac.Data.WriteUint16(0x1) + servicesSnac.Data.WriteUint16(0x4) + servicesFlap := oscar.NewFLAP(2) + servicesFlap.Data.WriteBinary(servicesSnac) + session.Send(servicesFlap) + + return ctx } else if flap.Header.Channel == 2 { snac := &oscar.SNAC{} err := snac.UnmarshalBinary(flap.Data.Bytes()) @@ -89,13 +117,20 @@ func main() { } if service, ok := services[snac.Header.Family]; ok { - err = service.HandleSNAC(db, session, snac) + newCtx, err := service.HandleSNAC(ctx, db, snac) util.PanicIfError(err) + return newCtx } + } else if flap.Header.Channel == 4 { + session.Disconnect() } + + return ctx }) RegisterService(0x17, &AuthorizationRegistrationService{}) + RegisterService(0x01, &GenericServiceControls{}) + RegisterService(0x04, &ICBM{}) 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 842c6e5..d339249 100644 --- a/models/User.go +++ b/models/User.go @@ -17,6 +17,16 @@ type User struct { Cipher string } +type userKey string + +func (s userKey) String() string { + return "user-" + string(s) +} + +var ( + currentUser = userKey("user") +) + func UserByUsername(ctx context.Context, db *bun.DB, username string) (*User, error) { user := new(User) if err := db.NewSelect().Model(user).Where("username = ?", username).Scan(ctx, user); err != nil { @@ -28,6 +38,29 @@ 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) { + user := new(User) + if err := db.NewSelect().Model(user).Where("uin = ?", uin).Scan(ctx, user); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, errors.Wrap(err, "could not fetch user") + } + return user, nil +} + +func NewContextWithUser(ctx context.Context, user *User) context.Context { + return context.WithValue(ctx, currentUser, user) +} + +func UserFromContext(ctx context.Context) *User { + v := ctx.Value(currentUser) + if v == nil { + return nil + } + return v.(*User) +} + func (u *User) Update(ctx context.Context, db *bun.DB) error { if _, err := db.NewUpdate().Model(u).WherePK("uin").Exec(ctx); err != nil { return errors.Wrap(err, "could not update user") diff --git a/oscar/server.go b/oscar/server.go index 1cb3786..5d5acc3 100644 --- a/oscar/server.go +++ b/oscar/server.go @@ -2,27 +2,31 @@ package oscar import ( "aim-oscar/util" - "fmt" + "context" + "encoding/binary" "io" "log" "net" + "strings" "github.com/pkg/errors" ) -type HandlerFunc func(*Session, *FLAP) +type HandlerFunc func(context.Context, *FLAP) context.Context -type Handler struct{ fn HandlerFunc } +type Handler struct{ handle HandlerFunc } func NewHandler(fn HandlerFunc) *Handler { return &Handler{ - fn: fn, + handle: fn, } } func (h *Handler) Handle(conn net.Conn) { - session := NewSession(conn) - buf := make([]byte, 1024) + ctx := NewContextWithSession(context.Background(), conn) + session, _ := SessionFromContext(ctx) + + buf := make([]byte, 2048) for { if !session.GreetedClient { // send a hello @@ -35,7 +39,13 @@ func (h *Handler) Handle(conn net.Conn) { n, err := conn.Read(buf) if err != nil && err != io.EOF { - log.Println("Read Error: ", err.Error()) + if strings.Contains(err.Error(), "use of closed network connection") { + log.Printf("%v disconnected", conn.RemoteAddr()) + session.Disconnect() + return + } + + log.Println("OSCAR Read Error: ", err.Error()) return } @@ -46,7 +56,7 @@ func (h *Handler) Handle(conn net.Conn) { // Try to parse all of the FLAPs in the buffer if we have enough bytes to // fill a FLAP header for len(buf) >= 6 && buf[0] == 0x2a { - dataLength := util.Word(buf[4:6]) + dataLength := binary.BigEndian.Uint16(buf[4:6]) flapLength := int(dataLength) + 6 if len(buf) < flapLength { log.Printf("not enough data, only %d bytes\n", len(buf)) @@ -58,8 +68,7 @@ func (h *Handler) Handle(conn net.Conn) { util.PanicIfError(errors.Wrap(err, "could not unmarshal FLAP")) } buf = buf[flapLength:] - fmt.Printf("%v ->\n%+v\n", conn.RemoteAddr(), flap) - h.fn(session, flap) + ctx = h.handle(ctx, flap) } } } diff --git a/oscar/session.go b/oscar/session.go index 093f648..8259448 100644 --- a/oscar/session.go +++ b/oscar/session.go @@ -20,14 +20,14 @@ var ( ) type Session struct { - Conn net.Conn + conn net.Conn SequenceNumber uint16 GreetedClient bool } func NewSession(conn net.Conn) *Session { return &Session{ - Conn: conn, + conn: conn, SequenceNumber: 0, GreetedClient: false, } @@ -38,7 +38,7 @@ func NewContextWithSession(ctx context.Context, conn net.Conn) context.Context { return context.WithValue(ctx, currentSession, session) } -func CurrentSession(ctx context.Context) (session *Session, err error) { +func SessionFromContext(ctx context.Context) (session *Session, err error) { s := ctx.Value(currentSession) if s == nil { return nil, errors.New("no session in context") @@ -46,6 +46,10 @@ func CurrentSession(ctx context.Context) (session *Session, err error) { return s.(*Session), nil } +func (s *Session) RemoteAddr() net.Addr { + return s.conn.RemoteAddr() +} + func (s *Session) Send(flap *FLAP) error { s.SequenceNumber += 1 flap.Header.SequenceNumber = s.SequenceNumber @@ -54,7 +58,11 @@ func (s *Session) Send(flap *FLAP) error { return errors.Wrap(err, "could not marshal message") } - fmt.Printf("-> %v\n%s\n\n", s.Conn.RemoteAddr(), util.PrettyBytes(bytes)) - _, err = s.Conn.Write(bytes) + fmt.Printf("-> %v\n%s\n\n", s.conn.RemoteAddr(), util.PrettyBytes(bytes)) + _, err = s.conn.Write(bytes) return errors.Wrap(err, "could not write to client connection") } + +func (s *Session) Disconnect() error { + return s.conn.Close() +} diff --git a/oscar/tlv.go b/oscar/tlv.go index d1bfa4d..010009b 100644 --- a/oscar/tlv.go +++ b/oscar/tlv.go @@ -43,8 +43,8 @@ func (t *TLV) UnmarshalBinary(data []byte) error { if len(data) < 4 { return io.ErrUnexpectedEOF } - t.Type = util.Word(data[:2]) - t.DataLength = util.Word(data[2:4]) + t.Type = binary.BigEndian.Uint16(data[:2]) + t.DataLength = binary.BigEndian.Uint16(data[2:4]) if len(data) < 4+int(t.DataLength) { return io.ErrUnexpectedEOF } diff --git a/service.go b/service.go index 26b2850..35d38bd 100644 --- a/service.go +++ b/service.go @@ -2,10 +2,11 @@ package main import ( "aim-oscar/oscar" + "context" "github.com/uptrace/bun" ) type Service interface { - HandleSNAC(*bun.DB, *oscar.Session, *oscar.SNAC) error + HandleSNAC(context.Context, *bun.DB, *oscar.SNAC) (context.Context, error) } diff --git a/util/util.go b/util/util.go index 6c04d50..48d113a 100644 --- a/util/util.go +++ b/util/util.go @@ -53,12 +53,10 @@ func PanicIfError(err error) { } } -func Word(b []byte) uint16 { - var _ = b[1] - return uint16(b[1]) | uint16(b[0])<<8 +func Word(x uint16) []byte { + return []byte{byte(x >> 8), byte(x & 0xf)} } -func DWord(b []byte) uint32 { - var _ = b[3] - return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +func Dword(x uint32) []byte { + return []byte{byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x & 0xf)} }