| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- package foodgroup
- import (
- "context"
- "errors"
- "log/slog"
- "net/mail"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // NewAdminService creates an instance of AdminService.
- func NewAdminService(
- accountManager AccountManager,
- bartItemManager BARTItemManager,
- relationshipFetcher RelationshipFetcher,
- messageRelayer MessageRelayer,
- sessionRetriever SessionRetriever,
- logger *slog.Logger,
- ) *AdminService {
- return &AdminService{
- accountManager: accountManager,
- buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
- messageRelayer: messageRelayer,
- logger: logger,
- }
- }
- // AdminService provides functionality for the Admin food group.
- // The Admin food group is used for client control of passwords, screen name formatting,
- // email address, and account confirmation.
- type AdminService struct {
- accountManager AccountManager
- buddyBroadcaster buddyBroadcaster
- messageRelayer MessageRelayer
- logger *slog.Logger
- }
- // ConfirmRequest will mark the user account as confirmed if the user has an email address set
- func (s AdminService) ConfirmRequest(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error) {
- // getAdminInfoReply returns an AdminAcctConfirmReply SNAC
- var getAdminConfirmReply = func(status uint16) wire.SNACMessage {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Admin,
- SubGroup: wire.AdminAcctConfirmReply,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNAC_0x07_0x07_AdminConfirmReply{
- Status: status,
- },
- }
- }
- _, err := s.accountManager.EmailAddress(ctx, instance.IdentScreenName())
- if errors.Is(err, state.ErrNoEmailAddress) {
- return getAdminConfirmReply(wire.AdminAcctConfirmStatusServerError), nil
- } else if err != nil {
- return wire.SNACMessage{}, err
- }
- accountConfirmed, err := s.accountManager.ConfirmStatus(ctx, instance.IdentScreenName())
- if err != nil {
- return wire.SNACMessage{}, err
- }
- if accountConfirmed {
- return getAdminConfirmReply(wire.AdminAcctConfirmStatusAlreadyConfirmed), nil
- }
- if err := s.accountManager.UpdateConfirmStatus(ctx, instance.IdentScreenName(), true); err != nil {
- return wire.SNACMessage{}, err
- }
- instance.ClearUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
- if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
- return wire.SNACMessage{}, err
- }
- return getAdminConfirmReply(wire.AdminAcctConfirmStatusEmailSent), nil
- }
- // InfoQuery returns the requested information about the account
- func (s AdminService) InfoQuery(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error) {
- // getAdminInfoReply returns an AdminInfoReply SNAC
- var getAdminInfoReply = func(tlvList wire.TLVList) wire.SNACMessage {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Admin,
- SubGroup: wire.AdminInfoReply,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNAC_0x07_0x03_AdminInfoReply{
- Permissions: wire.AdminInfoPermissionsReadWrite, // todo: what does this actually control?
- TLVBlock: wire.TLVBlock{
- TLVList: tlvList,
- },
- },
- }
- }
- tlvList := wire.TLVList{}
- if _, hasRegStatus := inBody.Bytes(wire.AdminTLVRegistrationStatus); hasRegStatus {
- regStatus, err := s.accountManager.RegStatus(ctx, instance.IdentScreenName())
- if err != nil {
- return wire.SNACMessage{}, err
- }
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVRegistrationStatus, regStatus))
- return getAdminInfoReply(tlvList), nil
- }
- if _, hasEmail := inBody.Bytes(wire.AdminTLVEmailAddress); hasEmail {
- e, err := s.accountManager.EmailAddress(ctx, instance.IdentScreenName())
- if errors.Is(err, state.ErrNoEmailAddress) {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, ""))
- } else if err != nil {
- return wire.SNACMessage{}, err
- } else {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, e.Address))
- }
- return getAdminInfoReply(tlvList), nil
- }
- if _, hasNickName := inBody.Bytes(wire.AdminTLVScreenNameFormatted); hasNickName {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, instance.DisplayScreenName().String()))
- return getAdminInfoReply(tlvList), nil
- }
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Admin,
- SubGroup: wire.AdminErr,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNACError{
- Code: wire.ErrorCodeNotSupportedByHost,
- },
- }, nil
- }
- // InfoChangeRequest handles the user changing account information
- func (s AdminService) InfoChangeRequest(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error) {
- // replyMessage builds and returns an AdminChangeReply SNAC
- var getAdminChangeReply = func(tlvList wire.TLVList) wire.SNACMessage {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Admin,
- SubGroup: wire.AdminInfoChangeReply,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNAC_0x07_0x05_AdminChangeReply{
- Permissions: wire.AdminInfoPermissionsReadWrite,
- TLVBlock: wire.TLVBlock{
- TLVList: tlvList,
- },
- },
- }
- }
- // validateProposedName ensures that the name is valid
- var validateProposedName = func(name state.DisplayScreenName) (ok bool, errorCode uint16) {
- err := name.ValidateAIMHandle()
- switch {
- case errors.Is(err, state.ErrAIMHandleLength):
- // proposed name is too long
- return false, wire.AdminInfoErrorInvalidNickNameLength
- case errors.Is(err, state.ErrAIMHandleInvalidFormat):
- // character or spacing issues
- return false, wire.AdminInfoErrorInvalidNickName
- }
- // proposed name does not match session name (e.g. malicious client)
- if name.IdentScreenName() != instance.IdentScreenName() {
- return false, wire.AdminInfoErrorValidateNickName
- }
- return true, 0
- }
- // validateProposedEmailAddress ensures that the email address is valid
- var validateProposedEmailAddress = func(emailAddress []byte) (e *mail.Address, errorCode uint16) {
- /*
- todo: pidgin/libpurple will show 'unknown error: 0xNNNN' for these error codes.
- We could do a client check here and send wire.AdminInfoErrorDNSFail so pidgin
- will show "given email address is invalid" instead.
- */
- e, err := mail.ParseAddress(string(emailAddress))
- // rfc 5322 basic validation
- if err != nil {
- return nil, wire.AdminInfoErrorInvalidEmail
- }
- // rfc 5521 length - local-part (64) + @ (1) + domain (255)
- if len(e.Address) > 320 {
- return nil, wire.AdminInfoErrorInvalidEmailLength
- }
- // todo: wire.AdminInfoErrorDNSFail could be sent here for an invalid domain name
- return e, 0
- }
- tlvList := wire.TLVList{}
- if sn, hasScreenNameFormatted := inBody.Bytes(wire.AdminTLVScreenNameFormatted); hasScreenNameFormatted {
- proposedName := state.DisplayScreenName(sn)
- if ok, errorCode := validateProposedName(proposedName); !ok {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, errorCode))
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
- return getAdminChangeReply(tlvList), nil
- }
- if err := s.accountManager.UpdateDisplayScreenName(ctx, proposedName); err != nil {
- return wire.SNACMessage{}, err
- }
- instance.Session().SetDisplayScreenName(proposedName)
- if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
- return wire.SNACMessage{}, err
- }
- s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- },
- Body: newOServiceUserInfoUpdate(instance),
- })
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, proposedName.String()))
- return getAdminChangeReply(tlvList), nil
- }
- if emailAddress, hasEmailAddress := inBody.Bytes(wire.AdminTLVEmailAddress); hasEmailAddress {
- e, errorCode := validateProposedEmailAddress(emailAddress)
- if errorCode != 0 {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, errorCode))
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
- return getAdminChangeReply(tlvList), nil
- }
- if err := s.accountManager.UpdateEmailAddress(ctx, instance.IdentScreenName(), e); err != nil {
- return wire.SNACMessage{}, err
- }
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVEmailAddress, e.Address))
- return getAdminChangeReply(tlvList), nil
- }
- if regStatus, hasRegStatus := inBody.Uint16BE(wire.AdminTLVRegistrationStatus); hasRegStatus {
- switch regStatus {
- case
- wire.AdminInfoRegStatusFullDisclosure,
- wire.AdminInfoRegStatusLimitDisclosure,
- wire.AdminInfoRegStatusNoDisclosure:
- if err := s.accountManager.UpdateRegStatus(ctx, instance.IdentScreenName(), regStatus); err != nil {
- return wire.SNACMessage{}, err
- }
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVRegistrationStatus, regStatus))
- return getAdminChangeReply(tlvList), nil
- }
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidRegistrationPreference))
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVUrl, ""))
- return getAdminChangeReply(tlvList), nil
- }
- // change password
- if newPass, hasPassStatus := inBody.String(wire.AdminTLVNewPassword); hasPassStatus {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}))
- oldPass, ok := inBody.String(wire.AdminTLVOldPassword)
- if !ok {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorNeedOldPassword))
- return getAdminChangeReply(tlvList), nil
- }
- u, err := s.accountManager.User(ctx, instance.IdentScreenName())
- if err != nil || u == nil {
- if err != nil {
- s.logger.ErrorContext(ctx, "accountManager.User: runtime error", "err", err)
- } else {
- s.logger.ErrorContext(ctx, "accountManager.User: can't find user", "err", err)
- }
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
- return getAdminChangeReply(tlvList), nil
- }
- if !u.ValidateHash(wire.StrongMD5PasswordHash(oldPass, u.AuthKey)) {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorValidatePassword))
- return getAdminChangeReply(tlvList), nil
- }
- if err := s.accountManager.SetUserPassword(ctx, instance.IdentScreenName(), newPass); err != nil {
- if errors.Is(err, state.ErrPasswordInvalid) {
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidPasswordLength))
- } else {
- s.logger.ErrorContext(ctx, "accountManager.SetUserPassword: runtime error", "err", err)
- tlvList.Append(wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors))
- }
- return getAdminChangeReply(tlvList), nil
- }
- return getAdminChangeReply(tlvList), nil
- }
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Admin,
- SubGroup: wire.AdminErr,
- RequestID: inFrame.RequestID,
- },
- Body: wire.SNACError{
- Code: wire.ErrorCodeNotSupportedByHost,
- },
- }, nil
- }
|