You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

database.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. // Copyright (c) 2012-2014 Jeremy Latt
  2. // Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
  3. // released under the MIT license
  4. package irc
  5. import (
  6. "encoding/json"
  7. "fmt"
  8. "log"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/oragono/oragono/irc/modes"
  14. "github.com/oragono/oragono/irc/utils"
  15. "github.com/tidwall/buntdb"
  16. )
  17. const (
  18. // 'version' of the database schema
  19. keySchemaVersion = "db.version"
  20. // latest schema of the db
  21. latestDbSchema = "10"
  22. )
  23. type SchemaChanger func(*Config, *buntdb.Tx) error
  24. type SchemaChange struct {
  25. InitialVersion string // the change will take this version
  26. TargetVersion string // and transform it into this version
  27. Changer SchemaChanger
  28. }
  29. // maps an initial version to a schema change capable of upgrading it
  30. var schemaChanges map[string]SchemaChange
  31. // InitDB creates the database, implementing the `oragono initdb` command.
  32. func InitDB(path string) {
  33. _, err := os.Stat(path)
  34. if err == nil {
  35. log.Fatal("Datastore already exists (delete it manually to continue): ", path)
  36. } else if !os.IsNotExist(err) {
  37. log.Fatal("Datastore path is inaccessible: ", err.Error())
  38. }
  39. err = initializeDB(path)
  40. if err != nil {
  41. log.Fatal("Could not save datastore: ", err.Error())
  42. }
  43. }
  44. // internal database initialization code
  45. func initializeDB(path string) error {
  46. store, err := buntdb.Open(path)
  47. if err != nil {
  48. return err
  49. }
  50. defer store.Close()
  51. err = store.Update(func(tx *buntdb.Tx) error {
  52. // set schema version
  53. tx.Set(keySchemaVersion, latestDbSchema, nil)
  54. return nil
  55. })
  56. return err
  57. }
  58. // OpenDatabase returns an existing database, performing a schema version check.
  59. func OpenDatabase(config *Config) (*buntdb.DB, error) {
  60. return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
  61. }
  62. // open the database, giving it at most one chance to auto-upgrade the schema
  63. func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
  64. db, err = buntdb.Open(config.Datastore.Path)
  65. if err != nil {
  66. return
  67. }
  68. defer func() {
  69. if err != nil && db != nil {
  70. db.Close()
  71. db = nil
  72. }
  73. }()
  74. // read the current version string
  75. var version string
  76. err = db.View(func(tx *buntdb.Tx) error {
  77. version, err = tx.Get(keySchemaVersion)
  78. return err
  79. })
  80. if err != nil {
  81. return
  82. }
  83. if version == latestDbSchema {
  84. // success
  85. return
  86. }
  87. // XXX quiesce the DB so we can be sure it's safe to make a backup copy
  88. db.Close()
  89. db = nil
  90. if allowAutoupgrade {
  91. err = performAutoUpgrade(version, config)
  92. if err != nil {
  93. return
  94. }
  95. // successful autoupgrade, let's try this again:
  96. return openDatabaseInternal(config, false)
  97. } else {
  98. err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
  99. return
  100. }
  101. }
  102. func performAutoUpgrade(currentVersion string, config *Config) (err error) {
  103. path := config.Datastore.Path
  104. log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
  105. timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
  106. backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
  107. log.Printf("making a backup of current database at %s\n", backupPath)
  108. err = utils.CopyFile(path, backupPath)
  109. if err != nil {
  110. return err
  111. }
  112. err = UpgradeDB(config)
  113. if err != nil {
  114. // database upgrade is a single transaction, so we don't need to restore the backup;
  115. // we can just delete it
  116. os.Remove(backupPath)
  117. }
  118. return err
  119. }
  120. // UpgradeDB upgrades the datastore to the latest schema.
  121. func UpgradeDB(config *Config) (err error) {
  122. // #715: test that the database exists
  123. _, err = os.Stat(config.Datastore.Path)
  124. if err != nil {
  125. return err
  126. }
  127. store, err := buntdb.Open(config.Datastore.Path)
  128. if err != nil {
  129. return err
  130. }
  131. defer store.Close()
  132. var version string
  133. err = store.Update(func(tx *buntdb.Tx) error {
  134. for {
  135. version, _ = tx.Get(keySchemaVersion)
  136. change, schemaNeedsChange := schemaChanges[version]
  137. if !schemaNeedsChange {
  138. if version == latestDbSchema {
  139. // success!
  140. break
  141. }
  142. // unable to upgrade to the desired version, roll back
  143. return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
  144. }
  145. log.Println("attempting to update schema from version " + version)
  146. err := change.Changer(config, tx)
  147. if err != nil {
  148. return err
  149. }
  150. _, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil)
  151. if err != nil {
  152. return err
  153. }
  154. log.Println("successfully updated schema to version " + change.TargetVersion)
  155. }
  156. return nil
  157. })
  158. if err != nil {
  159. log.Printf("database upgrade failed and was rolled back: %v\n", err)
  160. }
  161. return err
  162. }
  163. func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
  164. // == version 1 -> 2 ==
  165. // account key changes and account.verified key bugfix.
  166. var keysToRemove []string
  167. newKeys := make(map[string]string)
  168. tx.AscendKeys("account *", func(key, value string) bool {
  169. keysToRemove = append(keysToRemove, key)
  170. splitkey := strings.Split(key, " ")
  171. // work around bug
  172. if splitkey[2] == "exists" {
  173. // manually create new verified key
  174. newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1])
  175. newKeys[newVerifiedKey] = "1"
  176. } else if splitkey[1] == "%s" {
  177. return true
  178. }
  179. newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1])
  180. newKeys[newKey] = value
  181. return true
  182. })
  183. for _, key := range keysToRemove {
  184. tx.Delete(key)
  185. }
  186. for key, value := range newKeys {
  187. tx.Set(key, value, nil)
  188. }
  189. return nil
  190. }
  191. // 1. channel founder names should be casefolded
  192. // 2. founder should be explicitly granted the ChannelFounder user mode
  193. // 3. explicitly initialize stored channel modes to the server default values
  194. func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
  195. var channels []string
  196. prefix := "channel.exists "
  197. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  198. if !strings.HasPrefix(key, prefix) {
  199. return false
  200. }
  201. chname := strings.TrimPrefix(key, prefix)
  202. channels = append(channels, chname)
  203. return true
  204. })
  205. // founder names should be casefolded
  206. // founder should be explicitly granted the ChannelFounder user mode
  207. for _, channel := range channels {
  208. founderKey := "channel.founder " + channel
  209. founder, _ := tx.Get(founderKey)
  210. if founder != "" {
  211. founder, err := CasefoldName(founder)
  212. if err == nil {
  213. tx.Set(founderKey, founder, nil)
  214. accountToUmode := map[string]modes.Mode{
  215. founder: modes.ChannelFounder,
  216. }
  217. atustr, _ := json.Marshal(accountToUmode)
  218. tx.Set("channel.accounttoumode "+channel, string(atustr), nil)
  219. }
  220. }
  221. }
  222. // explicitly store the channel modes
  223. defaultModes := config.Channels.defaultModes
  224. modeStrings := make([]string, len(defaultModes))
  225. for i, mode := range defaultModes {
  226. modeStrings[i] = string(mode)
  227. }
  228. defaultModeString := strings.Join(modeStrings, "")
  229. for _, channel := range channels {
  230. tx.Set("channel.modes "+channel, defaultModeString, nil)
  231. }
  232. return nil
  233. }
  234. // 1. ban info format changed (from `legacyBanInfo` below to `IPBanInfo`)
  235. // 2. dlines against individual IPs are normalized into dlines against the appropriate /128 network
  236. func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error {
  237. type ipRestrictTime struct {
  238. Duration time.Duration
  239. Expires time.Time
  240. }
  241. type legacyBanInfo struct {
  242. Reason string `json:"reason"`
  243. OperReason string `json:"oper_reason"`
  244. OperName string `json:"oper_name"`
  245. Time *ipRestrictTime `json:"time"`
  246. }
  247. now := time.Now()
  248. legacyToNewInfo := func(old legacyBanInfo) (new_ IPBanInfo) {
  249. new_.Reason = old.Reason
  250. new_.OperReason = old.OperReason
  251. new_.OperName = old.OperName
  252. if old.Time == nil {
  253. new_.TimeCreated = now
  254. new_.Duration = 0
  255. } else {
  256. new_.TimeCreated = old.Time.Expires.Add(-1 * old.Time.Duration)
  257. new_.Duration = old.Time.Duration
  258. }
  259. return
  260. }
  261. var keysToDelete []string
  262. prefix := "bans.dline "
  263. dlines := make(map[string]IPBanInfo)
  264. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  265. if !strings.HasPrefix(key, prefix) {
  266. return false
  267. }
  268. keysToDelete = append(keysToDelete, key)
  269. var lbinfo legacyBanInfo
  270. id := strings.TrimPrefix(key, prefix)
  271. err := json.Unmarshal([]byte(value), &lbinfo)
  272. if err != nil {
  273. log.Printf("error unmarshaling legacy dline: %v\n", err)
  274. return true
  275. }
  276. // legacy keys can be either an IP or a CIDR
  277. hostNet, err := utils.NormalizedNetFromString(id)
  278. if err != nil {
  279. log.Printf("error unmarshaling legacy dline network: %v\n", err)
  280. return true
  281. }
  282. dlines[utils.NetToNormalizedString(hostNet)] = legacyToNewInfo(lbinfo)
  283. return true
  284. })
  285. setOptions := func(info IPBanInfo) *buntdb.SetOptions {
  286. if info.Duration == 0 {
  287. return nil
  288. }
  289. ttl := info.TimeCreated.Add(info.Duration).Sub(now)
  290. return &buntdb.SetOptions{Expires: true, TTL: ttl}
  291. }
  292. // store the new dlines
  293. for id, info := range dlines {
  294. b, err := json.Marshal(info)
  295. if err != nil {
  296. log.Printf("error marshaling migrated dline: %v\n", err)
  297. continue
  298. }
  299. tx.Set(fmt.Sprintf("bans.dlinev2 %s", id), string(b), setOptions(info))
  300. }
  301. // same operations against klines
  302. prefix = "bans.kline "
  303. klines := make(map[string]IPBanInfo)
  304. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  305. if !strings.HasPrefix(key, prefix) {
  306. return false
  307. }
  308. keysToDelete = append(keysToDelete, key)
  309. mask := strings.TrimPrefix(key, prefix)
  310. var lbinfo legacyBanInfo
  311. err := json.Unmarshal([]byte(value), &lbinfo)
  312. if err != nil {
  313. log.Printf("error unmarshaling legacy kline: %v\n", err)
  314. return true
  315. }
  316. klines[mask] = legacyToNewInfo(lbinfo)
  317. return true
  318. })
  319. for mask, info := range klines {
  320. b, err := json.Marshal(info)
  321. if err != nil {
  322. log.Printf("error marshaling migrated kline: %v\n", err)
  323. continue
  324. }
  325. tx.Set(fmt.Sprintf("bans.klinev2 %s", mask), string(b), setOptions(info))
  326. }
  327. // clean up all the old entries
  328. for _, key := range keysToDelete {
  329. tx.Delete(key)
  330. }
  331. return nil
  332. }
  333. // create new key tracking channels that belong to an account
  334. func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
  335. founderToChannels := make(map[string][]string)
  336. prefix := "channel.founder "
  337. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  338. if !strings.HasPrefix(key, prefix) {
  339. return false
  340. }
  341. channel := strings.TrimPrefix(key, prefix)
  342. founderToChannels[value] = append(founderToChannels[value], channel)
  343. return true
  344. })
  345. for founder, channels := range founderToChannels {
  346. tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil)
  347. }
  348. return nil
  349. }
  350. // custom nick enforcement was a separate db key, now it's part of settings
  351. func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
  352. accountToEnforcement := make(map[string]NickEnforcementMethod)
  353. prefix := "account.customenforcement "
  354. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  355. if !strings.HasPrefix(key, prefix) {
  356. return false
  357. }
  358. account := strings.TrimPrefix(key, prefix)
  359. method, err := nickReservationFromString(value)
  360. if err == nil {
  361. accountToEnforcement[account] = method
  362. } else {
  363. log.Printf("skipping corrupt custom enforcement value for %s\n", account)
  364. }
  365. return true
  366. })
  367. for account, method := range accountToEnforcement {
  368. var settings AccountSettings
  369. settings.NickEnforcement = method
  370. text, err := json.Marshal(settings)
  371. if err != nil {
  372. return err
  373. }
  374. tx.Delete(prefix + account)
  375. tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
  376. }
  377. return nil
  378. }
  379. type maskInfoV7 struct {
  380. TimeCreated time.Time
  381. CreatorNickmask string
  382. CreatorAccount string
  383. }
  384. func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
  385. now := time.Now().UTC()
  386. var channels []string
  387. prefix := "channel.exists "
  388. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  389. if !strings.HasPrefix(key, prefix) {
  390. return false
  391. }
  392. channels = append(channels, strings.TrimPrefix(key, prefix))
  393. return true
  394. })
  395. converter := func(key string) {
  396. oldRawValue, err := tx.Get(key)
  397. if err != nil {
  398. return
  399. }
  400. var masks []string
  401. err = json.Unmarshal([]byte(oldRawValue), &masks)
  402. if err != nil {
  403. return
  404. }
  405. newCookedValue := make(map[string]maskInfoV7)
  406. for _, mask := range masks {
  407. normalizedMask, err := CanonicalizeMaskWildcard(mask)
  408. if err != nil {
  409. continue
  410. }
  411. newCookedValue[normalizedMask] = maskInfoV7{
  412. TimeCreated: now,
  413. CreatorNickmask: "*",
  414. CreatorAccount: "*",
  415. }
  416. }
  417. newRawValue, err := json.Marshal(newCookedValue)
  418. if err != nil {
  419. return
  420. }
  421. tx.Set(key, string(newRawValue), nil)
  422. }
  423. prefixes := []string{
  424. "channel.banlist %s",
  425. "channel.exceptlist %s",
  426. "channel.invitelist %s",
  427. }
  428. for _, channel := range channels {
  429. for _, prefix := range prefixes {
  430. converter(fmt.Sprintf(prefix, channel))
  431. }
  432. }
  433. return nil
  434. }
  435. type accountSettingsLegacyV7 struct {
  436. AutoreplayLines *int
  437. NickEnforcement NickEnforcementMethod
  438. AllowBouncer MulticlientAllowedSetting
  439. AutoreplayJoins bool
  440. }
  441. type accountSettingsLegacyV8 struct {
  442. AutoreplayLines *int
  443. NickEnforcement NickEnforcementMethod
  444. AllowBouncer MulticlientAllowedSetting
  445. ReplayJoins ReplayJoinsSetting
  446. }
  447. // #616: change autoreplay-joins to replay-joins
  448. func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
  449. prefix := "account.settings "
  450. var accounts, blobs []string
  451. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  452. var legacy accountSettingsLegacyV7
  453. var current accountSettingsLegacyV8
  454. if !strings.HasPrefix(key, prefix) {
  455. return false
  456. }
  457. account := strings.TrimPrefix(key, prefix)
  458. err := json.Unmarshal([]byte(value), &legacy)
  459. if err != nil {
  460. log.Printf("corrupt record for %s: %v\n", account, err)
  461. return true
  462. }
  463. current.AutoreplayLines = legacy.AutoreplayLines
  464. current.NickEnforcement = legacy.NickEnforcement
  465. current.AllowBouncer = legacy.AllowBouncer
  466. if legacy.AutoreplayJoins {
  467. current.ReplayJoins = ReplayJoinsAlways
  468. } else {
  469. current.ReplayJoins = ReplayJoinsCommandsOnly
  470. }
  471. blob, err := json.Marshal(current)
  472. if err != nil {
  473. log.Printf("could not marshal record for %s: %v\n", account, err)
  474. return true
  475. }
  476. accounts = append(accounts, account)
  477. blobs = append(blobs, string(blob))
  478. return true
  479. })
  480. for i, account := range accounts {
  481. tx.Set(prefix+account, blobs[i], nil)
  482. }
  483. return nil
  484. }
  485. type accountCredsLegacyV8 struct {
  486. Version uint
  487. PassphraseSalt []byte // legacy field, not used by v1 and later
  488. PassphraseHash []byte
  489. Certificate string
  490. }
  491. type accountCredsLegacyV9 struct {
  492. Version uint
  493. PassphraseSalt []byte // legacy field, not used by v1 and later
  494. PassphraseHash []byte
  495. Certfps []string
  496. }
  497. // #530: support multiple client certificate fingerprints
  498. func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
  499. prefix := "account.credentials "
  500. var accounts, blobs []string
  501. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  502. var legacy accountCredsLegacyV8
  503. var current accountCredsLegacyV9
  504. if !strings.HasPrefix(key, prefix) {
  505. return false
  506. }
  507. account := strings.TrimPrefix(key, prefix)
  508. err := json.Unmarshal([]byte(value), &legacy)
  509. if err != nil {
  510. log.Printf("corrupt record for %s: %v\n", account, err)
  511. return true
  512. }
  513. current.Version = legacy.Version
  514. current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
  515. current.PassphraseHash = legacy.PassphraseHash
  516. if legacy.Certificate != "" {
  517. current.Certfps = []string{legacy.Certificate}
  518. }
  519. blob, err := json.Marshal(current)
  520. if err != nil {
  521. log.Printf("could not marshal record for %s: %v\n", account, err)
  522. return true
  523. }
  524. accounts = append(accounts, account)
  525. blobs = append(blobs, string(blob))
  526. return true
  527. })
  528. for i, account := range accounts {
  529. tx.Set(prefix+account, blobs[i], nil)
  530. }
  531. return nil
  532. }
  533. // #836: account registration time at nanosecond resolution
  534. // (mostly to simplify testing)
  535. func schemaChangeV9ToV10(config *Config, tx *buntdb.Tx) error {
  536. prefix := "account.registered.time "
  537. var accounts, times []string
  538. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  539. if !strings.HasPrefix(key, prefix) {
  540. return false
  541. }
  542. account := strings.TrimPrefix(key, prefix)
  543. accounts = append(accounts, account)
  544. times = append(times, value)
  545. return true
  546. })
  547. for i, account := range accounts {
  548. time, err := strconv.ParseInt(times[i], 10, 64)
  549. if err != nil {
  550. log.Printf("corrupt registration time entry for %s: %v\n", account, err)
  551. continue
  552. }
  553. time = time * 1000000000
  554. tx.Set(prefix+account, strconv.FormatInt(time, 10), nil)
  555. }
  556. return nil
  557. }
  558. func init() {
  559. allChanges := []SchemaChange{
  560. {
  561. InitialVersion: "1",
  562. TargetVersion: "2",
  563. Changer: schemaChangeV1toV2,
  564. },
  565. {
  566. InitialVersion: "2",
  567. TargetVersion: "3",
  568. Changer: schemaChangeV2ToV3,
  569. },
  570. {
  571. InitialVersion: "3",
  572. TargetVersion: "4",
  573. Changer: schemaChangeV3ToV4,
  574. },
  575. {
  576. InitialVersion: "4",
  577. TargetVersion: "5",
  578. Changer: schemaChangeV4ToV5,
  579. },
  580. {
  581. InitialVersion: "5",
  582. TargetVersion: "6",
  583. Changer: schemaChangeV5ToV6,
  584. },
  585. {
  586. InitialVersion: "6",
  587. TargetVersion: "7",
  588. Changer: schemaChangeV6ToV7,
  589. },
  590. {
  591. InitialVersion: "7",
  592. TargetVersion: "8",
  593. Changer: schemaChangeV7ToV8,
  594. },
  595. {
  596. InitialVersion: "8",
  597. TargetVersion: "9",
  598. Changer: schemaChangeV8ToV9,
  599. },
  600. {
  601. InitialVersion: "9",
  602. TargetVersion: "10",
  603. Changer: schemaChangeV9ToV10,
  604. },
  605. }
  606. // build the index
  607. schemaChanges = make(map[string]SchemaChange)
  608. for _, change := range allChanges {
  609. schemaChanges[change.InitialVersion] = change
  610. }
  611. }