admin.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), wire.SNACMessage{
  195. Frame: wire.SNACFrame{
  196. FoodGroup: wire.OService,
  197. SubGroup: wire.OServiceUserInfoUpdate,
  198. },
  199. Body: newOServiceUserInfoUpdate(instance),
  200. })
  201. tlvList.Append(wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, proposedName.String()))
  202. return getAdminChangeReply(tlvList), nil
  203. }
  204. if emailAddress, hasEmailAddress := inBody.Bytes(wire.AdminTLVEmailAddress); hasEmailAddress {
  205. e, errorCode := validateProposedEmailAddress(emailAddress)
  206. if errorCode != 0 {
  207. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, errorCode))
  208. tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
  209. return getAdminChangeReply(tlvList), nil
  210. }
  211. if err := s.accountManager.UpdateEmailAddress(ctx, instance.IdentScreenName(), e); err != nil {
  212. return wire.SNACMessage{}, err
  213. }
  214. tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, e.Address))
  215. return getAdminChangeReply(tlvList), nil
  216. }
  217. if regStatus, hasRegStatus := inBody.Uint16BE(wire.AdminTLVRegistrationStatus); hasRegStatus {
  218. switch regStatus {
  219. case
  220. wire.AdminInfoRegStatusFullDisclosure,
  221. wire.AdminInfoRegStatusLimitDisclosure,
  222. wire.AdminInfoRegStatusNoDisclosure:
  223. if err := s.accountManager.UpdateRegStatus(ctx, instance.IdentScreenName(), regStatus); err != nil {
  224. return wire.SNACMessage{}, err
  225. }
  226. tlvList.Append(wire.NewTLVBE(wire.AdminTLVRegistrationStatus, regStatus))
  227. return getAdminChangeReply(tlvList), nil
  228. }
  229. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidRegistrationPreference))
  230. tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
  231. return getAdminChangeReply(tlvList), nil
  232. }
  233. // change password
  234. if newPass, hasPassStatus := inBody.String(wire.AdminTLVNewPassword); hasPassStatus {
  235. tlvList.Append(wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}))
  236. oldPass, ok := inBody.String(wire.AdminTLVOldPassword)
  237. if !ok {
  238. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorNeedOldPassword))
  239. return getAdminChangeReply(tlvList), nil
  240. }
  241. u, err := s.accountManager.User(ctx, instance.IdentScreenName())
  242. if err != nil || u == nil {
  243. if err != nil {
  244. s.logger.ErrorContext(ctx, "accountManager.User: runtime error", "err", err)
  245. } else {
  246. s.logger.ErrorContext(ctx, "accountManager.User: can't find user", "err", err)
  247. }
  248. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
  249. return getAdminChangeReply(tlvList), nil
  250. }
  251. if !u.ValidateHash(wire.StrongMD5PasswordHash(oldPass, u.AuthKey)) {
  252. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorValidatePassword))
  253. return getAdminChangeReply(tlvList), nil
  254. }
  255. if err := s.accountManager.SetUserPassword(ctx, instance.IdentScreenName(), newPass); err != nil {
  256. if errors.Is(err, state.ErrPasswordInvalid) {
  257. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidPasswordLength))
  258. } else {
  259. s.logger.ErrorContext(ctx, "accountManager.SetUserPassword: runtime error", "err", err)
  260. tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
  261. }
  262. return getAdminChangeReply(tlvList), nil
  263. }
  264. return getAdminChangeReply(tlvList), nil
  265. }
  266. return wire.SNACMessage{
  267. Frame: wire.SNACFrame{
  268. FoodGroup: wire.Admin,
  269. SubGroup: wire.AdminErr,
  270. RequestID: inFrame.RequestID,
  271. },
  272. Body: wire.SNACError{
  273. Code: wire.ErrorCodeNotSupportedByHost,
  274. },
  275. }, nil
  276. }