auth.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package handler
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "github.com/google/uuid"
  7. "github.com/mk6i/retro-aim-server/config"
  8. "github.com/mk6i/retro-aim-server/oscar"
  9. "github.com/mk6i/retro-aim-server/state"
  10. )
  11. // NewAuthService creates a new instance of AuthService.
  12. func NewAuthService(cfg config.Config, sessionManager SessionManager, messageRelayer MessageRelayer, feedbagManager FeedbagManager, userManager UserManager, chatRegistry ChatRegistry) *AuthService {
  13. return &AuthService{
  14. chatRegistry: chatRegistry,
  15. config: cfg,
  16. feedbagManager: feedbagManager,
  17. messageRelayer: messageRelayer,
  18. sessionManager: sessionManager,
  19. userManager: userManager,
  20. }
  21. }
  22. // AuthService provides user BUCP login and session management services.
  23. type AuthService struct {
  24. chatRegistry ChatRegistry
  25. config config.Config
  26. feedbagManager FeedbagManager
  27. messageRelayer MessageRelayer
  28. sessionManager SessionManager
  29. userManager UserManager
  30. }
  31. // RetrieveChatSession returns a chat room session. Return nil if the session
  32. // does not exist.
  33. func (s AuthService) RetrieveChatSession(chatID string, sessionID string) (*state.Session, error) {
  34. _, chatSessMgr, err := s.chatRegistry.Retrieve(chatID)
  35. if err != nil {
  36. return nil, err
  37. }
  38. return chatSessMgr.(SessionManager).RetrieveSession(sessionID), nil
  39. }
  40. // RetrieveBOSSession returns a user's session. Return nil if the session does
  41. // not exist.
  42. func (s AuthService) RetrieveBOSSession(sessionID string) (*state.Session, error) {
  43. return s.sessionManager.RetrieveSession(sessionID), nil
  44. }
  45. // Signout removes user from the BOS server and notifies adjacent users (those
  46. // who have this user's screen name on their buddy list) of their departure.
  47. func (s AuthService) Signout(ctx context.Context, sess *state.Session) error {
  48. if err := broadcastDeparture(ctx, sess, s.messageRelayer, s.feedbagManager); err != nil {
  49. return err
  50. }
  51. s.sessionManager.RemoveSession(sess)
  52. return nil
  53. }
  54. // SignoutChat removes user from chat room and notifies remaining participants
  55. // of their departure. If user is the last to leave, the chat room is deleted.
  56. func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session, chatID string) error {
  57. chatRoom, chatSessMgr, err := s.chatRegistry.Retrieve(chatID)
  58. if err != nil {
  59. return err
  60. }
  61. alertUserLeft(ctx, sess, chatSessMgr.(ChatMessageRelayer))
  62. chatSessMgr.(SessionManager).RemoveSession(sess)
  63. if chatSessMgr.(SessionManager).Empty() {
  64. s.chatRegistry.Remove(chatRoom.Cookie)
  65. }
  66. return nil
  67. }
  68. // BUCPChallengeRequestHandler satisfies the client request for a random auth
  69. // key. It returns SNAC oscar.BUCPChallengeResponse. If the screen name is
  70. // recognized as a valid user, the response contains the account's auth key,
  71. // which salts the user's MD5 password hash. If the account is invalid, an
  72. // error code is set in TLV oscar.TLVErrorSubcode. If login credentials are
  73. // invalid and app config DisableAuth is true, a stub auth key is generated and
  74. // a successful challenge response is returned.
  75. func (s AuthService) BUCPChallengeRequestHandler(bodyIn oscar.SNAC_0x17_0x06_BUCPChallengeRequest, newUUIDFn func() uuid.UUID) (oscar.SNACMessage, error) {
  76. screenName, exists := bodyIn.String(oscar.TLVScreenName)
  77. if !exists {
  78. return oscar.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
  79. }
  80. var authKey string
  81. u, err := s.userManager.User(screenName)
  82. switch {
  83. case err != nil:
  84. return oscar.SNACMessage{}, err
  85. case u != nil:
  86. // user lookup succeeded
  87. authKey = u.AuthKey
  88. case s.config.DisableAuth:
  89. // can't find user, generate stub auth key
  90. authKey = newUUIDFn().String()
  91. default:
  92. // can't find user, return login error
  93. snacFrameOut := oscar.SNACFrame{
  94. FoodGroup: oscar.BUCP,
  95. SubGroup: oscar.BUCPLoginResponse,
  96. }
  97. snacPayloadOut := oscar.SNAC_0x17_0x03_BUCPLoginResponse{}
  98. snacPayloadOut.Append(oscar.NewTLV(oscar.TLVErrorSubcode, uint16(0x01)))
  99. return oscar.SNACMessage{
  100. Frame: snacFrameOut,
  101. Body: snacPayloadOut,
  102. }, nil
  103. }
  104. return oscar.SNACMessage{
  105. Frame: oscar.SNACFrame{
  106. FoodGroup: oscar.BUCP,
  107. SubGroup: oscar.BUCPChallengeResponse,
  108. },
  109. Body: oscar.SNAC_0x17_0x07_BUCPChallengeResponse{
  110. AuthKey: authKey,
  111. },
  112. }, nil
  113. }
  114. // BUCPLoginRequestHandler verifies user credentials. Upon successful login, a
  115. // session is created.
  116. // If login credentials are invalid and app config DisableAuth is true, a stub
  117. // user is created and login continues as normal. DisableAuth allows you to
  118. // skip the account creation procedure, which simplifies the login flow during
  119. // development.
  120. // If login is successful, the SNAC TLV list contains the BOS server address
  121. // (oscar.TLVReconnectHere) and an authorization cookie
  122. // (oscar.TLVAuthorizationCookie). Else, an error code is set
  123. // (oscar.TLVErrorSubcode).
  124. func (s AuthService) BUCPLoginRequestHandler(bodyIn oscar.SNAC_0x17_0x02_BUCPLoginRequest, newUUIDFn func() uuid.UUID, newUserFn func(screenName string) (state.User, error)) (oscar.SNACMessage, error) {
  125. screenName, found := bodyIn.String(oscar.TLVScreenName)
  126. if !found {
  127. return oscar.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
  128. }
  129. md5Hash, found := bodyIn.Slice(oscar.TLVPasswordHash)
  130. if !found {
  131. return oscar.SNACMessage{}, errors.New("password hash doesn't exist in tlv")
  132. }
  133. loginOK := false
  134. u, err := s.userManager.User(screenName)
  135. switch {
  136. case err != nil:
  137. return oscar.SNACMessage{}, err
  138. case u != nil && bytes.Equal(u.PassHash, md5Hash):
  139. // password check succeeded
  140. loginOK = true
  141. case s.config.DisableAuth:
  142. // login failed but let them in anyway
  143. user, err := newUserFn(screenName)
  144. if err != nil {
  145. return oscar.SNACMessage{}, err
  146. }
  147. if err := s.userManager.InsertUser(user); err != nil {
  148. return oscar.SNACMessage{}, err
  149. }
  150. loginOK = true
  151. }
  152. snacPayloadOut := oscar.SNAC_0x17_0x03_BUCPLoginResponse{}
  153. snacPayloadOut.Append(oscar.NewTLV(oscar.TLVScreenName, screenName))
  154. if loginOK {
  155. sess := s.sessionManager.AddSession(newUUIDFn().String(), screenName)
  156. snacPayloadOut.AppendList([]oscar.TLV{
  157. oscar.NewTLV(oscar.TLVReconnectHere, config.Address(s.config.OSCARHost, s.config.BOSPort)),
  158. oscar.NewTLV(oscar.TLVAuthorizationCookie, sess.ID()),
  159. })
  160. } else {
  161. snacPayloadOut.AppendList([]oscar.TLV{
  162. oscar.NewTLV(oscar.TLVErrorSubcode, uint16(0x01)),
  163. })
  164. }
  165. return oscar.SNACMessage{
  166. Frame: oscar.SNACFrame{
  167. FoodGroup: oscar.BUCP,
  168. SubGroup: oscar.BUCPLoginResponse,
  169. },
  170. Body: snacPayloadOut,
  171. }, nil
  172. }