user.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. package state
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "unicode"
  10. "github.com/google/uuid"
  11. "github.com/mk6i/retro-aim-server/wire"
  12. )
  13. var (
  14. // ErrDupUser indicates that a user already exists.
  15. ErrDupUser = errors.New("user already exists")
  16. // ErrNoUser indicates that a user does not exist.
  17. ErrNoUser = errors.New("user does not exist")
  18. // ErrNoEmail indicates that a user has not set an email address.
  19. ErrNoEmailAddress = errors.New("user has no email address")
  20. )
  21. // IdentScreenName struct stores the normalized version of a user's screen name.
  22. // This format is used for uniformity in storage and comparison by removing spaces
  23. // and converting all characters to lowercase.
  24. type IdentScreenName struct {
  25. // screenName contains the identifier screen name value. Do not assign this
  26. // value directly. Rather, set it through NewIdentScreenName. This ensures
  27. // that when an instance of IdentScreenName is present, it's guaranteed to
  28. // have a normalized value.
  29. screenName string
  30. }
  31. // String returns the string representation of the IdentScreenName.
  32. func (i IdentScreenName) String() string {
  33. return i.screenName
  34. }
  35. // UIN returns a numeric UIN representation of the IdentScreenName.
  36. func (i IdentScreenName) UIN() uint32 {
  37. v, _ := strconv.Atoi(i.screenName)
  38. return uint32(v)
  39. }
  40. // NewIdentScreenName creates a new IdentScreenName.
  41. func NewIdentScreenName(screenName string) IdentScreenName {
  42. str := strings.ReplaceAll(screenName, " ", "")
  43. str = strings.ToLower(str)
  44. return IdentScreenName{screenName: str}
  45. }
  46. // DisplayScreenName type represents the screen name in the user-defined format.
  47. // This includes the original casing and spacing as defined by the user.
  48. type DisplayScreenName string
  49. var (
  50. ErrAIMHandleInvalidFormat = errors.New("screen name must start with a letter, cannot end with a space, and must contain only letters, numbers, and spaces")
  51. ErrAIMHandleLength = errors.New("screen name must be between 3 and 16 characters")
  52. ErrPasswordInvalid = errors.New("invalid password length")
  53. ErrICQUINInvalidFormat = errors.New("uin must be a number in the range 10000-2147483646")
  54. )
  55. // ValidateAIMHandle returns an error if the instance is not a valid AIM screen name.
  56. // Possible errors:
  57. // - ErrAIMHandleLength: if the screen name has less than 3 non-space
  58. // characters or more than 16 characters (including spaces).
  59. // - ErrAIMHandleInvalidFormat: if the screen name does not start with a
  60. // letter, ends with a space, or contains invalid characters
  61. func (s DisplayScreenName) ValidateAIMHandle() error {
  62. if len(s) > 16 {
  63. return ErrAIMHandleLength
  64. }
  65. c := 0
  66. for _, r := range s {
  67. switch {
  68. case unicode.IsLetter(r) || unicode.IsDigit(r):
  69. c++
  70. case r != ' ':
  71. return ErrAIMHandleInvalidFormat
  72. }
  73. }
  74. if c < 3 {
  75. return ErrAIMHandleLength
  76. }
  77. // Must start with a letter, cannot end with a space, and must contain only
  78. // letters, numbers, and spaces.
  79. if !unicode.IsLetter(rune(s[0])) || s[len(s)-1] == ' ' {
  80. return ErrAIMHandleInvalidFormat
  81. }
  82. return nil
  83. }
  84. // IsUIN indicates whether the screen name is an ICQ UIN.
  85. func (s DisplayScreenName) IsUIN() bool {
  86. if len(s) == 0 {
  87. return false
  88. }
  89. for _, r := range s {
  90. if !unicode.IsDigit(r) {
  91. return false
  92. }
  93. }
  94. return true
  95. }
  96. // ValidateUIN returns an error if the instance is not a valid ICQ UIN.
  97. // Possible errors:
  98. // - ErrICQUINInvalidFormat: if the UIN is not a number or is not in the valid
  99. // range
  100. func (s DisplayScreenName) ValidateUIN() error {
  101. uin, err := strconv.Atoi(string(s))
  102. if err != nil || uin < 10000 || uin > 2147483646 {
  103. return ErrICQUINInvalidFormat
  104. }
  105. return nil
  106. }
  107. // IdentScreenName converts the DisplayScreenName to an IdentScreenName by applying
  108. // the normalization process defined in NewIdentScreenName.
  109. func (s DisplayScreenName) IdentScreenName() IdentScreenName {
  110. return NewIdentScreenName(string(s))
  111. }
  112. // String returns the original display string of the screen name, preserving the user-defined
  113. // casing and spaces.
  114. func (s DisplayScreenName) String() string {
  115. return string(s)
  116. }
  117. // NewStubUser creates a new user with canned credentials. The default password
  118. // is "welcome1". This is typically used for development purposes.
  119. func NewStubUser(screenName DisplayScreenName) (User, error) {
  120. uid, err := uuid.NewRandom()
  121. if err != nil {
  122. return User{}, err
  123. }
  124. u := User{
  125. IdentScreenName: NewIdentScreenName(string(screenName)),
  126. DisplayScreenName: screenName,
  127. AuthKey: uid.String(),
  128. IsICQ: screenName.IsUIN(),
  129. }
  130. err = u.HashPassword("welcome1")
  131. return u, err
  132. }
  133. // User represents a user account.
  134. type User struct {
  135. // IdentScreenName is the AIM screen name.
  136. IdentScreenName IdentScreenName
  137. // DisplayScreenName is the formatted screen name.
  138. DisplayScreenName DisplayScreenName
  139. // AuthKey is the salt for the MD5 password hash.
  140. AuthKey string
  141. // StrongMD5Pass is the MD5 password hash format used by AIM v4.8-v5.9.
  142. StrongMD5Pass []byte
  143. // WeakMD5Pass is the MD5 password hash format used by AIM v3.5-v4.7. This
  144. // hash is used to authenticate roasted passwords for AIM v1.0-v3.0.
  145. WeakMD5Pass []byte
  146. // IsICQ indicates whether the user is an ICQ account (true) or an AIM
  147. // account (false).
  148. IsICQ bool
  149. // ConfirmStatus indicates whether the user has confirmed their AIM account.
  150. ConfirmStatus bool
  151. // RegStatus is the AIM registration status.
  152. // 1: no disclosure
  153. // 2: limit disclosure
  154. // 3: full disclosure
  155. RegStatus int
  156. // SuspendedStatus is the account suspended status
  157. SuspendedStatus uint16
  158. // EmailAddress is the email address set by the AIM client.
  159. EmailAddress string
  160. // ICQAffiliations holds information about the user's affiliations,
  161. // including past and current affiliations.
  162. ICQAffiliations ICQAffiliations
  163. // ICQInterests holds information about the user's interests, categorized
  164. // by code and associated keywords.
  165. ICQInterests ICQInterests
  166. // ICQMoreInfo contains additional information about the user.
  167. ICQMoreInfo ICQMoreInfo
  168. // ICQPermissions specifies the user's privacy settings.
  169. ICQPermissions ICQPermissions
  170. // ICQBasicInfo contains the user's basic profile information, including
  171. // contact details and personal identifiers.
  172. ICQBasicInfo ICQBasicInfo
  173. // ICQNotes allows the user to store personal notes or additional
  174. // information within their profile.
  175. ICQNotes ICQUserNotes
  176. // ICQWorkInfo contains the user's professional information, including
  177. // their workplace address and job-related details.
  178. ICQWorkInfo ICQWorkInfo
  179. AIMDirectoryInfo AIMNameAndAddr
  180. // TOCConfig is the user's saved server-side info (buddy list, etc) for
  181. // on the TOC service.
  182. TOCConfig string
  183. // IsBot indicates whether the user is a bot.
  184. IsBot bool
  185. // LastWarnUpdate is the timestamp when the user's warning level was last updated.
  186. LastWarnUpdate time.Time
  187. // LastWarnLevel is the warning level when the user last signed off.
  188. LastWarnLevel uint16
  189. // OfflineMsgCount is the count of offline messages for the user.
  190. OfflineMsgCount int
  191. }
  192. // UserProfile represents a user's profile information.
  193. type UserProfile struct {
  194. // ProfileText is the free-form profile body content.
  195. ProfileText string
  196. // MIMEType is the MIME type of the profile content.
  197. MIMEType string
  198. // UpdateTime is when the profile was last updated.
  199. UpdateTime time.Time
  200. }
  201. // Empty returns true if the profile has not been set (all fields are zero values).
  202. func (p UserProfile) Empty() bool {
  203. return p.ProfileText == "" && p.MIMEType == "" && p.UpdateTime.IsZero()
  204. }
  205. // AIMNameAndAddr holds name and address AIM directory information.
  206. type AIMNameAndAddr struct {
  207. // FirstName is the user's first name.
  208. FirstName string
  209. // LastName is the user's last name.
  210. LastName string
  211. // MiddleName is the user's middle name.
  212. MiddleName string
  213. // MaidenName is the user's maiden name.
  214. MaidenName string
  215. // Country is the user's country of residence.
  216. Country string
  217. // State is the user's state or region of residence.
  218. State string
  219. // City is the user's city of residence.
  220. City string
  221. // NickName is the user's chosen nickname.
  222. NickName string
  223. // ZIPCode is the user's postal or ZIP code.
  224. ZIPCode string
  225. // Address is the user's street address.
  226. Address string
  227. }
  228. // ICQBasicInfo holds basic information about an ICQ user, including their name, contact details, and location.
  229. type ICQBasicInfo struct {
  230. // Address is the user's residential address.
  231. Address string
  232. // CellPhone is the user's mobile phone number.
  233. CellPhone string
  234. // City is the city where the user resides.
  235. City string
  236. // CountryCode is the code representing the user's country of residence.
  237. CountryCode uint16
  238. // EmailAddress is the user's primary email address.
  239. EmailAddress string
  240. // Fax is the user's fax number.
  241. Fax string
  242. // FirstName is the user's first name.
  243. FirstName string
  244. // GMTOffset is the user's time zone offset from GMT.
  245. GMTOffset uint8
  246. // LastName is the user's last name.
  247. LastName string
  248. // Nickname is the user's nickname or preferred name.
  249. Nickname string
  250. // Phone is the user's landline phone number.
  251. Phone string
  252. // PublishEmail indicates whether the user's email address is public.
  253. PublishEmail bool
  254. // State is the state or region where the user resides.
  255. State string
  256. // ZIPCode is the user's postal code.
  257. ZIPCode string
  258. }
  259. // ICQAffiliations contains information about the user's affiliations, both past and present.
  260. type ICQAffiliations struct {
  261. // PastCode1 is the code representing the user's first past affiliation.
  262. PastCode1 uint16
  263. // PastKeyword1 is the keyword associated with the user's first past affiliation.
  264. PastKeyword1 string
  265. // PastCode2 is the code representing the user's second past affiliation.
  266. PastCode2 uint16
  267. // PastKeyword2 is the keyword associated with the user's second past affiliation.
  268. PastKeyword2 string
  269. // PastCode3 is the code representing the user's third past affiliation.
  270. PastCode3 uint16
  271. // PastKeyword3 is the keyword associated with the user's third past affiliation.
  272. PastKeyword3 string
  273. // CurrentCode1 is the code representing the user's current first affiliation.
  274. CurrentCode1 uint16
  275. // CurrentKeyword1 is the keyword associated with the user's current first affiliation.
  276. CurrentKeyword1 string
  277. // CurrentCode2 is the code representing the user's current second affiliation.
  278. CurrentCode2 uint16
  279. // CurrentKeyword2 is the keyword associated with the user's current second affiliation.
  280. CurrentKeyword2 string
  281. // CurrentCode3 is the code representing the user's current third affiliation.
  282. CurrentCode3 uint16
  283. // CurrentKeyword3 is the keyword associated with the user's current third affiliation.
  284. CurrentKeyword3 string
  285. }
  286. // ICQInterests holds information about the user's interests, categorized by
  287. // interest code and associated keyword.
  288. type ICQInterests struct {
  289. // Code1 is the code representing the user's first interest.
  290. Code1 uint16
  291. // Keyword1 is the keyword associated with the user's first interest.
  292. Keyword1 string
  293. // Code2 is the code representing the user's second interest.
  294. Code2 uint16
  295. // Keyword2 is the keyword associated with the user's second interest.
  296. Keyword2 string
  297. // Code3 is the code representing the user's third interest.
  298. Code3 uint16
  299. // Keyword3 is the keyword associated with the user's third interest.
  300. Keyword3 string
  301. // Code4 is the code representing the user's fourth interest.
  302. Code4 uint16
  303. // Keyword4 is the keyword associated with the user's fourth interest.
  304. Keyword4 string
  305. }
  306. // ICQUserNotes contains personal notes or additional information added by the user.
  307. type ICQUserNotes struct {
  308. // Notes are the personal notes or additional information the user has
  309. // entered in their profile.
  310. Notes string
  311. }
  312. // ICQMoreInfo contains additional information about the user, such as
  313. // demographic and language preferences.
  314. type ICQMoreInfo struct {
  315. // Gender is the user's gender, represented by a code.
  316. Gender uint16
  317. // HomePageAddr is the URL of the user's personal homepage.
  318. HomePageAddr string
  319. // BirthYear is the user's birth year.
  320. BirthYear uint16
  321. // BirthMonth is the user's birth month.
  322. BirthMonth uint8
  323. // BirthDay is the user's birth day.
  324. BirthDay uint8
  325. // Lang1 is the code for the user's primary language.
  326. Lang1 uint8
  327. // Lang2 is the code for the user's secondary language.
  328. Lang2 uint8
  329. // Lang3 is the code for the user's tertiary language.
  330. Lang3 uint8
  331. }
  332. // ICQWorkInfo contains information about the user's professional life,
  333. // including their workplace and job title.
  334. type ICQWorkInfo struct {
  335. // Address is the address of the user's workplace.
  336. Address string
  337. // City is the city where the user's workplace is located.
  338. City string
  339. // Company is the name of the user's employer or company.
  340. Company string
  341. // CountryCode is the code representing the country where the user's
  342. // workplace is located.
  343. CountryCode uint16
  344. // Department is the name of the department within the user's company.
  345. Department string
  346. // Fax is the fax number for the user's workplace.
  347. Fax string
  348. // OccupationCode is the code representing the user's occupation.
  349. OccupationCode uint16
  350. // Phone is the phone number for the user's workplace.
  351. Phone string
  352. // Position is the user's job title or position within the company.
  353. Position string
  354. // State is the state or region where the user's workplace is located.
  355. State string
  356. // WebPage is the URL of the user's company's website.
  357. WebPage string
  358. // ZIPCode is the postal code for the user's workplace.
  359. ZIPCode string
  360. }
  361. // ICQPermissions specifies the privacy settings of an ICQ user.
  362. type ICQPermissions struct {
  363. // AuthRequired indicates where users must ask this permission to add them
  364. // to their contact list.
  365. AuthRequired bool
  366. }
  367. // Age returns the user's age relative to their birthday and timeNow.
  368. func (u *User) Age(timeNow func() time.Time) uint16 {
  369. now := timeNow().UTC()
  370. switch {
  371. case u.ICQMoreInfo.BirthYear > 0 && u.ICQMoreInfo.BirthDay == 0 && u.ICQMoreInfo.BirthMonth == 0:
  372. bday := time.Date(int(u.ICQMoreInfo.BirthYear), time.January, 1, 0, 0, 0, 0, time.UTC)
  373. return uint16(now.Year() - bday.Year())
  374. case u.ICQMoreInfo.BirthYear > 0 && u.ICQMoreInfo.BirthDay > 0 && u.ICQMoreInfo.BirthMonth > 0:
  375. bday := time.Date(int(u.ICQMoreInfo.BirthYear), time.Month(u.ICQMoreInfo.BirthMonth), int(u.ICQMoreInfo.BirthDay), 0, 0, 0, 0, time.UTC)
  376. years := now.Year() - bday.Year()
  377. if now.YearDay() < bday.YearDay() {
  378. years--
  379. }
  380. return uint16(years)
  381. default: // invalid date
  382. return 0
  383. }
  384. }
  385. // ValidateHash validates MD5-hashed passwords for BUCP auth. It handles
  386. // hashes used in early AIM 4.x versions ("weak" hashes) and later AIM 4.x-5.x
  387. // versions ("strong" hashes).
  388. func (u *User) ValidateHash(md5Hash []byte) bool {
  389. return bytes.Equal(u.StrongMD5Pass, md5Hash) || bytes.Equal(u.WeakMD5Pass, md5Hash)
  390. }
  391. // ValidateRoastedPass validates roasted passwords for FLAP auth.
  392. func (u *User) ValidateRoastedPass(roastedPass []byte) bool {
  393. clearPass := wire.RoastOSCARPassword(roastedPass)
  394. md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
  395. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  396. }
  397. // ValidateRoastedJavaPass validates roasted passwords for the Java AIM client FLAP auth.
  398. func (u *User) ValidateRoastedJavaPass(roastedPass []byte) bool {
  399. clearPass := wire.RoastOSCARJavaPassword(roastedPass)
  400. md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
  401. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  402. }
  403. // ValidateRoastedTOCPass validates roasted passwords for TOC auth.
  404. func (u *User) ValidateRoastedTOCPass(roastedPass []byte) bool {
  405. clearPass := wire.RoastTOCPassword(roastedPass)
  406. md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
  407. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  408. }
  409. // ValidatePlaintextPass validates plaintext passwords used in Kerberos auth.
  410. func (u *User) ValidatePlaintextPass(plaintextPass []byte) bool {
  411. md5Hash := wire.WeakMD5PasswordHash(string(plaintextPass), u.AuthKey)
  412. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  413. }
  414. // ValidateRoastedKerberosPass validates roasted passwords used in Kerberos auth.
  415. func (u *User) ValidateRoastedKerberosPass(roastedPass []byte) bool {
  416. clearPass := wire.RoastKerberosPassword(roastedPass)
  417. md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
  418. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  419. }
  420. // HashPassword computes MD5 hashes of the user's password. It computes both
  421. // weak and strong variants and stores them in the struct.
  422. func (u *User) HashPassword(passwd string) error {
  423. if u.IsICQ {
  424. if err := validateICQPassword(passwd); err != nil {
  425. return err
  426. }
  427. } else {
  428. if err := validateAIMPassword(passwd); err != nil {
  429. return err
  430. }
  431. }
  432. u.WeakMD5Pass = wire.WeakMD5PasswordHash(passwd, u.AuthKey)
  433. u.StrongMD5Pass = wire.StrongMD5PasswordHash(passwd, u.AuthKey)
  434. return nil
  435. }
  436. // validateAIMPassword returns an error if the AIM password is invalid.
  437. // A valid password is 4-16 characters long. The min and max password length
  438. // values reflect AOL's password validation rules circa 2000.
  439. func validateAIMPassword(pass string) error {
  440. if len(pass) < 4 || len(pass) > 16 {
  441. return fmt.Errorf("%w: password length must be between 4-16 characters", ErrPasswordInvalid)
  442. }
  443. return nil
  444. }
  445. // validateICQPassword returns an error if the ICQ password is invalid.
  446. // A valid password is 6-8 characters long. It's unclear what min length the
  447. // ICQ service required, so a plausible minimum value is set. The max length
  448. // reflects the password limitation imposed by old ICQ clients.
  449. func validateICQPassword(pass string) error {
  450. if len(pass) < 6 || len(pass) > 8 {
  451. return fmt.Errorf("%w: password must be between 6-8 characters", ErrPasswordInvalid)
  452. }
  453. return nil
  454. }
  455. type OfflineMessage struct {
  456. Sender IdentScreenName
  457. Recipient IdentScreenName
  458. Message wire.SNAC_0x04_0x06_ICBMChannelMsgToHost
  459. Sent time.Time
  460. }
  461. // Category represents an AIM directory category.
  462. type Category struct {
  463. // ID is the category ID
  464. ID uint8
  465. // Name is the category name
  466. Name string `oscar:"len_prefix=uint16"`
  467. }
  468. // Keyword represents an AIM directory keyword.
  469. type Keyword struct {
  470. // ID is the keyword ID
  471. ID uint8
  472. // Name is the keyword name
  473. Name string `oscar:"len_prefix=uint16"`
  474. }