user.go 16 KB

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