user_store.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. package state
  2. import (
  3. "bytes"
  4. "database/sql"
  5. "embed"
  6. "errors"
  7. "fmt"
  8. "io/fs"
  9. "net/http"
  10. "time"
  11. "github.com/mk6i/retro-aim-server/wire"
  12. "github.com/golang-migrate/migrate/v4"
  13. "github.com/golang-migrate/migrate/v4/database/sqlite3"
  14. "github.com/golang-migrate/migrate/v4/source/httpfs"
  15. "github.com/google/uuid"
  16. _ "github.com/mattn/go-sqlite3"
  17. )
  18. // BlockedState represents the blocked status between two users
  19. type BlockedState int
  20. //go:embed migrations/*
  21. var migrations embed.FS
  22. var (
  23. // ErrDupUser indicates that a user already exists.
  24. ErrDupUser = errors.New("user already exists")
  25. // ErrNoUser indicates that a user does not exist.
  26. ErrNoUser = errors.New("user does not exist")
  27. )
  28. const (
  29. // BlockedNo indicates that neither user blocks the other.
  30. BlockedNo BlockedState = iota
  31. // BlockedA indicates that user A blocks user B.
  32. BlockedA
  33. // BlockedB indicates that user B blocks user A.
  34. BlockedB
  35. )
  36. // User represents a user account.
  37. type User struct {
  38. // ScreenName is the AIM screen name.
  39. ScreenName string `json:"screen_name"`
  40. // AuthKey is the salt for the MD5 password hash.
  41. AuthKey string `json:"-"`
  42. // StrongMD5Pass is the MD5 password hash format used by AIM v4.8-v5.9.
  43. StrongMD5Pass []byte `json:"-"`
  44. // WeakMD5Pass is the MD5 password hash format used by AIM v3.5-v4.7. This
  45. // hash is used to authenticate roasted passwords for AIM v1.0-v3.0.
  46. WeakMD5Pass []byte `json:"-"`
  47. }
  48. // ValidateHash checks if md5Hash is identical to one of the password hashes.
  49. func (u *User) ValidateHash(md5Hash []byte) bool {
  50. return bytes.Equal(u.StrongMD5Pass, md5Hash) || bytes.Equal(u.WeakMD5Pass, md5Hash)
  51. }
  52. // ValidateRoastedPass checks if the provided roasted password matches the MD5
  53. // hash of the user's actual password. A roasted password is a XOR-obfuscated
  54. // form of the real password, intended to add a simple layer of security.
  55. func (u *User) ValidateRoastedPass(roastedPass []byte) bool {
  56. clearPass := wire.RoastPassword(roastedPass)
  57. md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
  58. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  59. }
  60. // HashPassword computes MD5 hashes of the user's password. It computes both
  61. // weak and strong variants and stores them in the struct.
  62. func (u *User) HashPassword(passwd string) error {
  63. u.WeakMD5Pass = wire.WeakMD5PasswordHash(passwd, u.AuthKey)
  64. u.StrongMD5Pass = wire.StrongMD5PasswordHash(passwd, u.AuthKey)
  65. return nil
  66. }
  67. // SQLiteUserStore stores user feedbag (buddy list), profile, and
  68. // authentication credentials information in a SQLite database.
  69. type SQLiteUserStore struct {
  70. db *sql.DB
  71. }
  72. // NewSQLiteUserStore creates a new instance of SQLiteUserStore. If the
  73. // database does not already exist, a new one is created with the required
  74. // schema.
  75. func NewSQLiteUserStore(dbFilePath string) (*SQLiteUserStore, error) {
  76. db, err := sql.Open("sqlite3", dbFilePath)
  77. if err != nil {
  78. return nil, err
  79. }
  80. store := &SQLiteUserStore{db: db}
  81. return store, store.runMigrations()
  82. }
  83. func (f SQLiteUserStore) runMigrations() error {
  84. migrationFS, err := fs.Sub(migrations, "migrations")
  85. if err != nil {
  86. return fmt.Errorf("failed to prepare migration subdirectory: %v", err)
  87. }
  88. sourceInstance, err := httpfs.New(http.FS(migrationFS), ".")
  89. if err != nil {
  90. return fmt.Errorf("failed to create source instance from embedded filesystem: %v", err)
  91. }
  92. driver, err := sqlite3.WithInstance(f.db, &sqlite3.Config{})
  93. if err != nil {
  94. return fmt.Errorf("cannot create database driver: %v", err)
  95. }
  96. m, err := migrate.NewWithInstance("httpfs", sourceInstance, "sqlite3", driver)
  97. if err != nil {
  98. return fmt.Errorf("failed to create migrate instance: %v", err)
  99. }
  100. if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
  101. return fmt.Errorf("failed to run migrations: %v", err)
  102. }
  103. return nil
  104. }
  105. // AllUsers returns all stored users. It only populates the User.ScreenName field
  106. // populated in the returned slice.
  107. func (f SQLiteUserStore) AllUsers() ([]User, error) {
  108. q := `SELECT screenName FROM user`
  109. rows, err := f.db.Query(q)
  110. if err != nil {
  111. return nil, err
  112. }
  113. defer rows.Close()
  114. var users []User
  115. for rows.Next() {
  116. u := User{}
  117. if err := rows.Scan(&u.ScreenName); err != nil {
  118. return nil, err
  119. }
  120. users = append(users, u)
  121. }
  122. if err := rows.Err(); err != nil {
  123. return nil, err
  124. }
  125. return users, nil
  126. }
  127. // User looks up a user by screen name. It populates the User record with
  128. // credentials that can be used to validate the user's password.
  129. func (f SQLiteUserStore) User(screenName string) (*User, error) {
  130. q := `
  131. SELECT
  132. screenName,
  133. authKey,
  134. weakMD5Pass,
  135. strongMD5Pass
  136. FROM user
  137. WHERE screenName = ?
  138. `
  139. u := &User{}
  140. err := f.db.QueryRow(q, screenName).Scan(&u.ScreenName, &u.AuthKey, &u.WeakMD5Pass, &u.StrongMD5Pass)
  141. if errors.Is(err, sql.ErrNoRows) {
  142. return nil, nil
  143. }
  144. return u, err
  145. }
  146. // InsertUser inserts a user to the store. Return ErrDupUser if a user with the
  147. // same screen name already exists.
  148. func (f SQLiteUserStore) InsertUser(u User) error {
  149. q := `
  150. INSERT INTO user (screenName, authKey, weakMD5Pass, strongMD5Pass)
  151. VALUES (?, ?, ?, ?)
  152. ON CONFLICT (screenName) DO NOTHING
  153. `
  154. result, err := f.db.Exec(q, u.ScreenName, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass)
  155. if err != nil {
  156. return err
  157. }
  158. rowsAffected, err := result.RowsAffected()
  159. if err != nil {
  160. return err
  161. }
  162. if rowsAffected == 0 {
  163. return ErrDupUser
  164. }
  165. return nil
  166. }
  167. // DeleteUser deletes a user from the store. Return ErrNoUser if the user did
  168. // not exist prior to deletion.
  169. func (f SQLiteUserStore) DeleteUser(screenName string) error {
  170. q := `
  171. DELETE FROM user WHERE screenName = ?
  172. `
  173. result, err := f.db.Exec(q, screenName)
  174. if err != nil {
  175. return err
  176. }
  177. rowsAffected, err := result.RowsAffected()
  178. if err != nil {
  179. return err
  180. }
  181. if rowsAffected == 0 {
  182. return ErrNoUser
  183. }
  184. return nil
  185. }
  186. // SetUserPassword sets the user's password hashes and auth key.
  187. func (f SQLiteUserStore) SetUserPassword(u User) error {
  188. tx, err := f.db.Begin()
  189. if err != nil {
  190. return err
  191. }
  192. defer func() {
  193. if err != nil {
  194. tx.Rollback()
  195. }
  196. }()
  197. q := `
  198. UPDATE user
  199. SET authKey = ?, weakMD5Pass = ?, strongMD5Pass = ?
  200. WHERE screenName = ?
  201. `
  202. result, err := tx.Exec(q, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass, u.ScreenName)
  203. if err != nil {
  204. return err
  205. }
  206. rowsAffected, err := result.RowsAffected()
  207. if err != nil {
  208. return err
  209. }
  210. if rowsAffected == 0 {
  211. // it's possible the user didn't change OR the user doesn't exist.
  212. // check if the user exists.
  213. var exists int
  214. err = tx.QueryRow("SELECT COUNT(*) FROM user WHERE screenName = ?", u.ScreenName).Scan(&exists)
  215. if err != nil {
  216. return err // Handle possible SQL errors during the select
  217. }
  218. if exists == 0 {
  219. return ErrNoUser // User does not exist
  220. }
  221. }
  222. return tx.Commit()
  223. }
  224. // Feedbag fetches the contents of a user's feedbag (buddy list).
  225. func (f SQLiteUserStore) Feedbag(screenName string) ([]wire.FeedbagItem, error) {
  226. q := `
  227. SELECT
  228. groupID,
  229. itemID,
  230. classID,
  231. name,
  232. attributes
  233. FROM feedbag
  234. WHERE screenName = ?
  235. `
  236. rows, err := f.db.Query(q, screenName)
  237. if err != nil {
  238. return nil, err
  239. }
  240. defer rows.Close()
  241. var items []wire.FeedbagItem
  242. for rows.Next() {
  243. var item wire.FeedbagItem
  244. var attrs []byte
  245. if err := rows.Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs); err != nil {
  246. return nil, err
  247. }
  248. if err := wire.Unmarshal(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
  249. return items, err
  250. }
  251. items = append(items, item)
  252. }
  253. return items, nil
  254. }
  255. // FeedbagLastModified returns the last time a user's feedbag (buddy list) was
  256. // updated.
  257. func (f SQLiteUserStore) FeedbagLastModified(screenName string) (time.Time, error) {
  258. var lastModified sql.NullInt64
  259. q := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
  260. err := f.db.QueryRow(q, screenName).Scan(&lastModified)
  261. return time.Unix(lastModified.Int64, 0), err
  262. }
  263. // FeedbagDelete deletes an entry from a user's feedbag (buddy list).
  264. func (f SQLiteUserStore) FeedbagDelete(screenName string, items []wire.FeedbagItem) error {
  265. // todo add transaction
  266. q := `DELETE FROM feedbag WHERE screenName = ? AND itemID = ?`
  267. for _, item := range items {
  268. if _, err := f.db.Exec(q, screenName, item.ItemID); err != nil {
  269. return err
  270. }
  271. }
  272. return nil
  273. }
  274. // FeedbagUpsert upserts an entry to a user's feedbag (buddy list). An entry is
  275. // created if it doesn't already exist, or modified if it already exists.
  276. func (f SQLiteUserStore) FeedbagUpsert(screenName string, items []wire.FeedbagItem) error {
  277. q := `
  278. INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, lastModified)
  279. VALUES (?, ?, ?, ?, ?, ?, UNIXEPOCH())
  280. ON CONFLICT (screenName, groupID, itemID)
  281. DO UPDATE SET classID = excluded.classID,
  282. name = excluded.name,
  283. attributes = excluded.attributes,
  284. lastModified = UNIXEPOCH()
  285. `
  286. for _, item := range items {
  287. buf := &bytes.Buffer{}
  288. if err := wire.Marshal(item.TLVLBlock, buf); err != nil {
  289. return err
  290. }
  291. _, err := f.db.Exec(q,
  292. screenName,
  293. item.GroupID,
  294. item.ItemID,
  295. item.ClassID,
  296. item.Name,
  297. buf.Bytes())
  298. if err != nil {
  299. return err
  300. }
  301. }
  302. return nil
  303. }
  304. // AdjacentUsers returns all users who have screenName in their buddy list.
  305. // Exclude users who are on screenName's block list.
  306. func (f SQLiteUserStore) AdjacentUsers(screenName string) ([]string, error) {
  307. q := `
  308. SELECT f.screenName
  309. FROM feedbag f
  310. WHERE f.name = ?
  311. AND f.classID = 0
  312. -- Don't show screenName that its blocked buddy is online
  313. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.screenName AND classID = 3)
  314. -- Don't show blocked buddy that screenName is online
  315. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.screenName AND name = f.name AND classID = 3)
  316. `
  317. rows, err := f.db.Query(q, screenName, screenName, screenName)
  318. if err != nil {
  319. return nil, err
  320. }
  321. defer rows.Close()
  322. var items []string
  323. for rows.Next() {
  324. var sn string
  325. if err := rows.Scan(&sn); err != nil {
  326. return nil, err
  327. }
  328. items = append(items, sn)
  329. }
  330. return items, nil
  331. }
  332. // Buddies returns all user's buddies. Don't return a buddy if the user has
  333. // them on their block list.
  334. func (f SQLiteUserStore) Buddies(screenName string) ([]string, error) {
  335. q := `
  336. SELECT f.name
  337. FROM feedbag f
  338. WHERE f.screenName = ? AND f.classID = 0
  339. -- Don't include buddy if they blocked screenName
  340. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.name AND name = ? AND classID = 3)
  341. -- Don't include buddy if screen name blocked them
  342. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.name AND classID = 3)
  343. `
  344. rows, err := f.db.Query(q, screenName, screenName, screenName)
  345. if err != nil {
  346. return nil, err
  347. }
  348. defer rows.Close()
  349. var items []string
  350. for rows.Next() {
  351. var sn string
  352. if err := rows.Scan(&sn); err != nil {
  353. return nil, err
  354. }
  355. items = append(items, sn)
  356. }
  357. return items, nil
  358. }
  359. // BlockedState returns the BlockedState between two users.
  360. func (f SQLiteUserStore) BlockedState(screenNameA, screenNameB string) (BlockedState, error) {
  361. q := `
  362. SELECT EXISTS(SELECT 1
  363. FROM feedbag f
  364. WHERE f.classID = 3
  365. AND f.screenName = ?
  366. AND f.name = ?)
  367. UNION ALL
  368. SELECT EXISTS(SELECT 1
  369. FROM feedbag f
  370. WHERE f.classID = 3
  371. AND f.screenName = ?
  372. AND f.name = ?)
  373. `
  374. row, err := f.db.Query(q, screenNameA, screenNameB, screenNameB, screenNameA)
  375. if err != nil {
  376. return BlockedNo, err
  377. }
  378. defer row.Close()
  379. var blockedA bool
  380. if row.Next() {
  381. if err := row.Scan(&blockedA); err != nil {
  382. return BlockedNo, err
  383. }
  384. }
  385. var blockedB bool
  386. if row.Next() {
  387. if err := row.Scan(&blockedB); err != nil {
  388. return BlockedNo, err
  389. }
  390. }
  391. switch {
  392. case blockedA:
  393. return BlockedA, nil
  394. case blockedB:
  395. return BlockedB, nil
  396. default:
  397. return BlockedNo, nil
  398. }
  399. }
  400. // Profile fetches a user profile. Return empty string if the user
  401. // does not exist or has no profile.
  402. func (f SQLiteUserStore) Profile(screenName string) (string, error) {
  403. q := `
  404. SELECT IFNULL(body, '')
  405. FROM profile
  406. WHERE screenName = ?
  407. `
  408. var profile string
  409. err := f.db.QueryRow(q, screenName).Scan(&profile)
  410. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  411. return "", err
  412. }
  413. return profile, nil
  414. }
  415. // SetProfile sets the text contents of a user's profile.
  416. func (f SQLiteUserStore) SetProfile(screenName string, body string) error {
  417. q := `
  418. INSERT INTO profile (screenName, body)
  419. VALUES (?, ?)
  420. ON CONFLICT (screenName)
  421. DO UPDATE SET body = excluded.body
  422. `
  423. _, err := f.db.Exec(q, screenName, body)
  424. return err
  425. }
  426. func (f SQLiteUserStore) BARTUpsert(itemHash []byte, body []byte) error {
  427. q := `
  428. INSERT INTO bartItem (hash, body)
  429. VALUES (?, ?)
  430. ON CONFLICT DO NOTHING
  431. `
  432. _, err := f.db.Exec(q, itemHash, body)
  433. return err
  434. }
  435. func (f SQLiteUserStore) BARTRetrieve(hash []byte) ([]byte, error) {
  436. q := `
  437. SELECT body
  438. FROM bartItem
  439. WHERE hash = ?
  440. `
  441. var body []byte
  442. err := f.db.QueryRow(q, hash).Scan(&body)
  443. if errors.Is(err, sql.ErrNoRows) {
  444. err = nil
  445. }
  446. return body, nil
  447. }
  448. // NewStubUser creates a new user with canned credentials. The default password
  449. // is "welcome1". This is typically used for development purposes.
  450. func NewStubUser(screenName string) (User, error) {
  451. uid, err := uuid.NewRandom()
  452. if err != nil {
  453. return User{}, err
  454. }
  455. u := User{
  456. ScreenName: screenName,
  457. AuthKey: uid.String(),
  458. }
  459. err = u.HashPassword("welcome1")
  460. return u, err
  461. }