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 16KB

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