admin.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package foodgroup
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "net/mail"
  7. "github.com/mk6i/open-oscar-server/state"
  8. "github.com/mk6i/open-oscar-server/wire"
  9. )
  10. // NewAdminService creates an instance of AdminService.
  11. func NewAdminService(
  12. accountManager AccountManager,
  13. bartItemManager BARTItemManager,
  14. relationshipFetcher RelationshipFetcher,
  15. messageRelayer MessageRelayer,
  16. sessionRetriever SessionRetriever,
  17. logger *slog.Logger,
  18. ) *AdminService {
  19. return &AdminService{
  20. accountManager: accountManager,
  21. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  22. messageRelayer: messageRelayer,
  23. logger: logger,
  24. }
  25. }
  26. // AdminService provides functionality for the Admin food group.
  27. // The Admin food group is used for client control of passwords, screen name formatting,
  28. // email address, and account confirmation.
  29. type AdminService struct {
  30. accountManager AccountManager
  31. buddyBroadcaster buddyBroadcaster
  32. messageRelayer MessageRelayer
  33. logger *slog.Logger
  34. }
  35. // ConfirmRequest will mark the user account as confirmed if the user has an email address set
  36. func (s AdminService) ConfirmRequest(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error) {
  37. // getAdminInfoReply returns an AdminAcctConfirmReply SNAC
  38. var getAdminConfirmReply = func(status uint16) wire.SNACMessage {
  39. return wire.SNACMessage{
  40. Frame: wire.SNACFrame{
  41. FoodGroup: wire.Admin,
  42. SubGroup: wire.AdminAcctConfirmReply,
  43. RequestID: inFrame.RequestID,
  44. },
  45. Body: wire.SNAC_0x07_0x07_AdminConfirmReply{
  46. Status: status,
  47. },
  48. }
  49. }
  50. _, err := s.accountManager.EmailAddress(ctx, instance.IdentScreenName())
  51. if errors.Is(err, state.ErrNoEmailAddress) {
  52. return getAdminConfirmReply(wire.AdminAcctConfirmStatusServerError), nil
  53. } else if err != nil {
  54. return wire.SNACMessage{}, err
  55. }
  56. accountConfirmed, err := s.accountManager.ConfirmStatus(ctx, instance.IdentScreenName())
  57. if err != nil {
  58. return wire.SNACMessage{}, err
  59. }
  60. if accountConfirmed {
  61. return getAdminConfirmReply(wire.AdminAcctConfirmStatusAlreadyConfirmed), nil
  62. }
  63. if err := s.accountManager.UpdateConfirmStatus(ctx, instance.IdentScreenName(), true); err != nil {
  64. return wire.SNACMessage{}, err
  65. }
  66. instance.ClearUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
  67. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  68. return wire.SNACMessage{}, err
  69. }
  70. return getAdminConfirmReply(wire.AdminAcctConfirmStatusEmailSent), nil
  71. }
  72. // InfoQuery returns the requested information about the account
  73. func (s AdminService) InfoQuery(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error) {
  74. // getAdminInfoReply returns an AdminInfoReply SNAC
  75. var getAdminInfoReply = func(tlvList wire.TLVList) wire.SNACMessage {
  76. return wire.SNACMessage{
  77. Frame: wire.SNACFrame{
  78. FoodGroup: wire.Admin,
  79. SubGroup: wire.AdminInfoReply,
  80. RequestID: inFrame.RequestID,
  81. },
  82. Body: wire.SNAC_0x07_0x03_AdminInfoReply{
  83. Permissions: wire.AdminInfoPermissionsReadWrite, // todo: what does this actually control?
  84. TLVBlock: wire.TLVBlock{
  85. TLVList: tlvList,
  86. },
  87. },
  88. }
  89. }
  90. tlvList := wire.TLVList{}
  91. if _, hasRegStatus := inBody.Bytes(wire.AdminTLVRegistrationStatus); hasRegStatus {
  92. regStatus, err := s.accountManager.RegStatus(ctx, instance.IdentScreenName())
  93. if err != nil {
  94. return wire.SNACMessage{}, err
  95. }
  96. tlvList.Append(wire.NewTLVBE(wire.AdminTLVRegistrationStatus, regStatus))
  97. return getAdminInfoReply(tlvList), nil
  98. }
  99. if _, hasEmail := inBody.Bytes(wire.AdminTLVEmailAddress); hasEmail {
  100. e, err := s.accountManager.EmailAddress(ctx, instance.IdentScreenName())
  101. if errors.Is(err, state.ErrNoEmailAddress) {
  102. tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, ""))
  103. } else if err != nil {
  104. return wire.SNACMessage{}, err
  105. } else {
  106. tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, e.Address))
  107. }
  108. return getAdminInfoReply(tlvList), nil
  109. }
  110. if _, hasNickName := inBody.Bytes(wire.AdminTLVScreenNameFormatted); hasNickName {
  111. tlvList.Append(wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, instance.DisplayScreenName().String()))
  112. return getAdminInfoReply(tlvList), nil
  113. }
  114. return wire.SNACMessage{
  115. Frame: wire.SNACFrame{
  116. FoodGroup: wire.Admin,
  117. SubGroup: wire.AdminErr,
  118. RequestID: inFrame.RequestID,
  119. },
  120. Body: wire.SNACError{
  121. Code: wire.ErrorCodeNotSupportedByHost,
  122. },
  123. }, nil
  124. }
  125. // InfoChangeRequest handles the user changing account information
  126. func (s AdminService) InfoChangeRequest(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error) {
  127. // replyMessage builds and returns an AdminChangeReply SNAC
  128. var getAdminChangeReply = func(tlvList wire.TLVList) wire.SNACMessage {
  129. return wire.SNACMessage{
  130. Frame: wire.SNACFrame{
  131. FoodGroup: wire.Admin,
  132. SubGroup: wire.AdminInfoChangeReply,
  133. RequestID: inFrame.RequestID,
  134. },
  135. Body: wire.SNAC_0x07_0x05_AdminChangeReply{
  136. Permissions: wire.AdminInfoPermissionsReadWrite,
  137. TLVBlock: wire.TLVBlock{
  138. TLVList: tlvList,
  139. },
  140. },
  141. }
  142. }
  143. // validateProposedName ensures that the name is valid
  144. var validateProposedName = func(name state.DisplayScreenName) (ok bool, errorCode uint16) {
  145. err := name.ValidateAIMHandle()
  146. switch {
  147. case errors.Is(err, state.ErrAIMHandleLength):
  148. // proposed name is too long
  149. return false, wire.AdminInfoErrorInvalidNickNameLength
  150. case errors.Is(err, state.ErrAIMHandleInvalidFormat):
  151. // character or spacing issues
  152. return false, wire.AdminInfoErrorInvalidNickName
  153. }
  154. // proposed name does not match session name (e.g. malicious client)
  155. if name.IdentScreenName() != instance.IdentScreenName() {
  156. return false, wire.AdminInfoErrorValidateNickName
  157. }
  158. return true, 0
  159. }
  160. // validateProposedEmailAddress ensures that the email address is valid
  161. var validateProposedEmailAddress = func(emailAddress []byte) (e *mail.Address, errorCode uint16) {
  162. /*
  163. todo: pidgin/libpurple will show 'unknown error: 0xNNNN' for these error codes.
  164. We could do a client check here and send wire.AdminInfoErrorDNSFail so pidgin
  165. will show "given email address is invalid" instead.
  166. */
  167. e, err := mail.ParseAddress(string(emailAddress))
  168. // rfc 5322 basic validation
  169. if err != nil {
  170. return nil, wire.AdminInfoErrorInvalidEmail
  171. }
  172. // rfc 5521 length - local-part (64) + @ (1) + domain (255)
  173. if len(e.Address) > 320 {
  174. return nil, wire.AdminInfoErrorInvalidEmailLength
  175. }
  176. // todo: wire.AdminInfoErrorDNSFail could be sent here for an invalid domain name
  177. return e, 0
  178. }
  179. tlvList := wire.TLVList{}
  180. if sn, hasScreenNameFormatted := inBody.Bytes(wire.AdminTLVScreenNameFormatted); hasScreenNameFormatted {
  181. proposedName := state.DisplayScreenName(sn)
  182. if ok, errorCode := validateProposedName(proposedName); !ok {
  183. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, errorCode))
  184. tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
  185. return getAdminChangeReply(tlvList), nil
  186. }
  187. if err := s.accountManager.UpdateDisplayScreenName(ctx, proposedName); err != nil {
  188. return wire.SNACMessage{}, err
  189. }
  190. instance.Session().SetDisplayScreenName(proposedName)
  191. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  192. return wire.SNACMessage{}, err
  193. }
  194. sendUserInfoUpdate(ctx, s.messageRelayer, instance)
  195. tlvList.Append(wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, proposedName.String()))
  196. return getAdminChangeReply(tlvList), nil
  197. }
  198. if emailAddress, hasEmailAddress := inBody.Bytes(wire.AdminTLVEmailAddress); hasEmailAddress {
  199. e, errorCode := validateProposedEmailAddress(emailAddress)
  200. if errorCode != 0 {
  201. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, errorCode))
  202. tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
  203. return getAdminChangeReply(tlvList), nil
  204. }
  205. if err := s.accountManager.UpdateEmailAddress(ctx, instance.IdentScreenName(), e); err != nil {
  206. return wire.SNACMessage{}, err
  207. }
  208. tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, e.Address))
  209. return getAdminChangeReply(tlvList), nil
  210. }
  211. if regStatus, hasRegStatus := inBody.Uint16BE(wire.AdminTLVRegistrationStatus); hasRegStatus {
  212. switch regStatus {
  213. case
  214. wire.AdminInfoRegStatusFullDisclosure,
  215. wire.AdminInfoRegStatusLimitDisclosure,
  216. wire.AdminInfoRegStatusNoDisclosure:
  217. if err := s.accountManager.UpdateRegStatus(ctx, instance.IdentScreenName(), regStatus); err != nil {
  218. return wire.SNACMessage{}, err
  219. }
  220. tlvList.Append(wire.NewTLVBE(wire.AdminTLVRegistrationStatus, regStatus))
  221. return getAdminChangeReply(tlvList), nil
  222. }
  223. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidRegistrationPreference))
  224. tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
  225. return getAdminChangeReply(tlvList), nil
  226. }
  227. // change password
  228. if newPass, hasPassStatus := inBody.String(wire.AdminTLVNewPassword); hasPassStatus {
  229. tlvList.Append(wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}))
  230. oldPass, ok := inBody.String(wire.AdminTLVOldPassword)
  231. if !ok {
  232. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorNeedOldPassword))
  233. return getAdminChangeReply(tlvList), nil
  234. }
  235. u, err := s.accountManager.User(ctx, instance.IdentScreenName())
  236. if err != nil || u == nil {
  237. if err != nil {
  238. s.logger.ErrorContext(ctx, "accountManager.User: runtime error", "err", err)
  239. } else {
  240. s.logger.ErrorContext(ctx, "accountManager.User: can't find user", "err", err)
  241. }
  242. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
  243. return getAdminChangeReply(tlvList), nil
  244. }
  245. if !u.ValidateHash(wire.StrongMD5PasswordHash(oldPass, u.AuthKey)) {
  246. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorValidatePassword))
  247. return getAdminChangeReply(tlvList), nil
  248. }
  249. if err := s.accountManager.SetUserPassword(ctx, instance.IdentScreenName(), newPass); err != nil {
  250. if errors.Is(err, state.ErrPasswordInvalid) {
  251. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidPasswordLength))
  252. } else {
  253. s.logger.ErrorContext(ctx, "accountManager.SetUserPassword: runtime error", "err", err)
  254. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
  255. }
  256. return getAdminChangeReply(tlvList), nil
  257. }
  258. return getAdminChangeReply(tlvList), nil
  259. }
  260. return wire.SNACMessage{
  261. Frame: wire.SNACFrame{
  262. FoodGroup: wire.Admin,
  263. SubGroup: wire.AdminErr,
  264. RequestID: inFrame.RequestID,
  265. },
  266. Body: wire.SNACError{
  267. Code: wire.ErrorCodeNotSupportedByHost,
  268. },
  269. }, nil
  270. }