user.go 19 KB

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