4
0

admin.go 9.0 KB

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