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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. // #715: test that the database exists
  122. _, err = os.Stat(config.Datastore.Path)
  123. if err != nil {
  124. return err
  125. }
  126. store, err := buntdb.Open(config.Datastore.Path)
  127. if err != nil {
  128. return err
  129. }
  130. defer store.Close()
  131. var version string
  132. err = store.Update(func(tx *buntdb.Tx) error {
  133. for {
  134. version, _ = tx.Get(keySchemaVersion)
  135. change, schemaNeedsChange := schemaChanges[version]
  136. if !schemaNeedsChange {
  137. if version == latestDbSchema {
  138. // success!
  139. break
  140. }
  141. // unable to upgrade to the desired version, roll back
  142. return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
  143. }
  144. log.Println("attempting to update schema from version " + version)
  145. err := change.Changer(config, tx)
  146. if err != nil {
  147. return err
  148. }
  149. _, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil)
  150. if err != nil {
  151. return err
  152. }
  153. log.Println("successfully updated schema to version " + change.TargetVersion)
  154. }
  155. return nil
  156. })
  157. if err != nil {
  158. log.Printf("database upgrade failed and was rolled back: %v\n", err)
  159. }
  160. return err
  161. }
  162. func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
  163. // == version 1 -> 2 ==
  164. // account key changes and account.verified key bugfix.
  165. var keysToRemove []string
  166. newKeys := make(map[string]string)
  167. tx.AscendKeys("account *", func(key, value string) bool {
  168. keysToRemove = append(keysToRemove, key)
  169. splitkey := strings.Split(key, " ")
  170. // work around bug
  171. if splitkey[2] == "exists" {
  172. // manually create new verified key
  173. newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1])
  174. newKeys[newVerifiedKey] = "1"
  175. } else if splitkey[1] == "%s" {
  176. return true
  177. }
  178. newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1])
  179. newKeys[newKey] = value
  180. return true
  181. })
  182. for _, key := range keysToRemove {
  183. tx.Delete(key)
  184. }
  185. for key, value := range newKeys {
  186. tx.Set(key, value, nil)
  187. }
  188. return nil
  189. }
  190. // 1. channel founder names should be casefolded
  191. // 2. founder should be explicitly granted the ChannelFounder user mode
  192. // 3. explicitly initialize stored channel modes to the server default values
  193. func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
  194. var channels []string
  195. prefix := "channel.exists "
  196. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  197. if !strings.HasPrefix(key, prefix) {
  198. return false
  199. }
  200. chname := strings.TrimPrefix(key, prefix)
  201. channels = append(channels, chname)
  202. return true
  203. })
  204. // founder names should be casefolded
  205. // founder should be explicitly granted the ChannelFounder user mode
  206. for _, channel := range channels {
  207. founderKey := "channel.founder " + channel
  208. founder, _ := tx.Get(founderKey)
  209. if founder != "" {
  210. founder, err := CasefoldName(founder)
  211. if err == nil {
  212. tx.Set(founderKey, founder, nil)
  213. accountToUmode := map[string]modes.Mode{
  214. founder: modes.ChannelFounder,
  215. }
  216. atustr, _ := json.Marshal(accountToUmode)
  217. tx.Set("channel.accounttoumode "+channel, string(atustr), nil)
  218. }
  219. }
  220. }
  221. // explicitly store the channel modes
  222. defaultModes := config.Channels.defaultModes
  223. modeStrings := make([]string, len(defaultModes))
  224. for i, mode := range defaultModes {
  225. modeStrings[i] = string(mode)
  226. }
  227. defaultModeString := strings.Join(modeStrings, "")
  228. for _, channel := range channels {
  229. tx.Set("channel.modes "+channel, defaultModeString, nil)
  230. }
  231. return nil
  232. }
  233. // 1. ban info format changed (from `legacyBanInfo` below to `IPBanInfo`)
  234. // 2. dlines against individual IPs are normalized into dlines against the appropriate /128 network
  235. func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error {
  236. type ipRestrictTime struct {
  237. Duration time.Duration
  238. Expires time.Time
  239. }
  240. type legacyBanInfo struct {
  241. Reason string `json:"reason"`
  242. OperReason string `json:"oper_reason"`
  243. OperName string `json:"oper_name"`
  244. Time *ipRestrictTime `json:"time"`
  245. }
  246. now := time.Now()
  247. legacyToNewInfo := func(old legacyBanInfo) (new_ IPBanInfo) {
  248. new_.Reason = old.Reason
  249. new_.OperReason = old.OperReason
  250. new_.OperName = old.OperName
  251. if old.Time == nil {
  252. new_.TimeCreated = now
  253. new_.Duration = 0
  254. } else {
  255. new_.TimeCreated = old.Time.Expires.Add(-1 * old.Time.Duration)
  256. new_.Duration = old.Time.Duration
  257. }
  258. return
  259. }
  260. var keysToDelete []string
  261. prefix := "bans.dline "
  262. dlines := make(map[string]IPBanInfo)
  263. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  264. if !strings.HasPrefix(key, prefix) {
  265. return false
  266. }
  267. keysToDelete = append(keysToDelete, key)
  268. var lbinfo legacyBanInfo
  269. id := strings.TrimPrefix(key, prefix)
  270. err := json.Unmarshal([]byte(value), &lbinfo)
  271. if err != nil {
  272. log.Printf("error unmarshaling legacy dline: %v\n", err)
  273. return true
  274. }
  275. // legacy keys can be either an IP or a CIDR
  276. hostNet, err := utils.NormalizedNetFromString(id)
  277. if err != nil {
  278. log.Printf("error unmarshaling legacy dline network: %v\n", err)
  279. return true
  280. }
  281. dlines[utils.NetToNormalizedString(hostNet)] = legacyToNewInfo(lbinfo)
  282. return true
  283. })
  284. setOptions := func(info IPBanInfo) *buntdb.SetOptions {
  285. if info.Duration == 0 {
  286. return nil
  287. }
  288. ttl := info.TimeCreated.Add(info.Duration).Sub(now)
  289. return &buntdb.SetOptions{Expires: true, TTL: ttl}
  290. }
  291. // store the new dlines
  292. for id, info := range dlines {
  293. b, err := json.Marshal(info)
  294. if err != nil {
  295. log.Printf("error marshaling migrated dline: %v\n", err)
  296. continue
  297. }
  298. tx.Set(fmt.Sprintf("bans.dlinev2 %s", id), string(b), setOptions(info))
  299. }
  300. // same operations against klines
  301. prefix = "bans.kline "
  302. klines := make(map[string]IPBanInfo)
  303. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  304. if !strings.HasPrefix(key, prefix) {
  305. return false
  306. }
  307. keysToDelete = append(keysToDelete, key)
  308. mask := strings.TrimPrefix(key, prefix)
  309. var lbinfo legacyBanInfo
  310. err := json.Unmarshal([]byte(value), &lbinfo)
  311. if err != nil {
  312. log.Printf("error unmarshaling legacy kline: %v\n", err)
  313. return true
  314. }
  315. klines[mask] = legacyToNewInfo(lbinfo)
  316. return true
  317. })
  318. for mask, info := range klines {
  319. b, err := json.Marshal(info)
  320. if err != nil {
  321. log.Printf("error marshaling migrated kline: %v\n", err)
  322. continue
  323. }
  324. tx.Set(fmt.Sprintf("bans.klinev2 %s", mask), string(b), setOptions(info))
  325. }
  326. // clean up all the old entries
  327. for _, key := range keysToDelete {
  328. tx.Delete(key)
  329. }
  330. return nil
  331. }
  332. // create new key tracking channels that belong to an account
  333. func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
  334. founderToChannels := make(map[string][]string)
  335. prefix := "channel.founder "
  336. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  337. if !strings.HasPrefix(key, prefix) {
  338. return false
  339. }
  340. channel := strings.TrimPrefix(key, prefix)
  341. founderToChannels[value] = append(founderToChannels[value], channel)
  342. return true
  343. })
  344. for founder, channels := range founderToChannels {
  345. tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil)
  346. }
  347. return nil
  348. }
  349. // custom nick enforcement was a separate db key, now it's part of settings
  350. func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
  351. accountToEnforcement := make(map[string]NickEnforcementMethod)
  352. prefix := "account.customenforcement "
  353. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  354. if !strings.HasPrefix(key, prefix) {
  355. return false
  356. }
  357. account := strings.TrimPrefix(key, prefix)
  358. method, err := nickReservationFromString(value)
  359. if err == nil {
  360. accountToEnforcement[account] = method
  361. } else {
  362. log.Printf("skipping corrupt custom enforcement value for %s\n", account)
  363. }
  364. return true
  365. })
  366. for account, method := range accountToEnforcement {
  367. var settings AccountSettings
  368. settings.NickEnforcement = method
  369. text, err := json.Marshal(settings)
  370. if err != nil {
  371. return err
  372. }
  373. tx.Delete(prefix + account)
  374. tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
  375. }
  376. return nil
  377. }
  378. type maskInfoV7 struct {
  379. TimeCreated time.Time
  380. CreatorNickmask string
  381. CreatorAccount string
  382. }
  383. func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
  384. now := time.Now().UTC()
  385. var channels []string
  386. prefix := "channel.exists "
  387. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  388. if !strings.HasPrefix(key, prefix) {
  389. return false
  390. }
  391. channels = append(channels, strings.TrimPrefix(key, prefix))
  392. return true
  393. })
  394. converter := func(key string) {
  395. oldRawValue, err := tx.Get(key)
  396. if err != nil {
  397. return
  398. }
  399. var masks []string
  400. err = json.Unmarshal([]byte(oldRawValue), &masks)
  401. if err != nil {
  402. return
  403. }
  404. newCookedValue := make(map[string]maskInfoV7)
  405. for _, mask := range masks {
  406. normalizedMask, err := CanonicalizeMaskWildcard(mask)
  407. if err != nil {
  408. continue
  409. }
  410. newCookedValue[normalizedMask] = maskInfoV7{
  411. TimeCreated: now,
  412. CreatorNickmask: "*",
  413. CreatorAccount: "*",
  414. }
  415. }
  416. newRawValue, err := json.Marshal(newCookedValue)
  417. if err != nil {
  418. return
  419. }
  420. tx.Set(key, string(newRawValue), nil)
  421. }
  422. prefixes := []string{
  423. "channel.banlist %s",
  424. "channel.exceptlist %s",
  425. "channel.invitelist %s",
  426. }
  427. for _, channel := range channels {
  428. for _, prefix := range prefixes {
  429. converter(fmt.Sprintf(prefix, channel))
  430. }
  431. }
  432. return nil
  433. }
  434. type accountSettingsLegacyV7 struct {
  435. AutoreplayLines *int
  436. NickEnforcement NickEnforcementMethod
  437. AllowBouncer MulticlientAllowedSetting
  438. AutoreplayJoins bool
  439. }
  440. type accountSettingsLegacyV8 struct {
  441. AutoreplayLines *int
  442. NickEnforcement NickEnforcementMethod
  443. AllowBouncer MulticlientAllowedSetting
  444. ReplayJoins ReplayJoinsSetting
  445. }
  446. // #616: change autoreplay-joins to replay-joins
  447. func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
  448. prefix := "account.settings "
  449. var accounts, blobs []string
  450. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  451. var legacy accountSettingsLegacyV7
  452. var current accountSettingsLegacyV8
  453. if !strings.HasPrefix(key, prefix) {
  454. return false
  455. }
  456. account := strings.TrimPrefix(key, prefix)
  457. err := json.Unmarshal([]byte(value), &legacy)
  458. if err != nil {
  459. log.Printf("corrupt record for %s: %v\n", account, err)
  460. return true
  461. }
  462. current.AutoreplayLines = legacy.AutoreplayLines
  463. current.NickEnforcement = legacy.NickEnforcement
  464. current.AllowBouncer = legacy.AllowBouncer
  465. if legacy.AutoreplayJoins {
  466. current.ReplayJoins = ReplayJoinsAlways
  467. } else {
  468. current.ReplayJoins = ReplayJoinsCommandsOnly
  469. }
  470. blob, err := json.Marshal(current)
  471. if err != nil {
  472. log.Printf("could not marshal record for %s: %v\n", account, err)
  473. return true
  474. }
  475. accounts = append(accounts, account)
  476. blobs = append(blobs, string(blob))
  477. return true
  478. })
  479. for i, account := range accounts {
  480. tx.Set(prefix+account, blobs[i], nil)
  481. }
  482. return nil
  483. }
  484. type accountCredsLegacyV8 struct {
  485. Version uint
  486. PassphraseSalt []byte // legacy field, not used by v1 and later
  487. PassphraseHash []byte
  488. Certificate string
  489. }
  490. type accountCredsLegacyV9 struct {
  491. Version uint
  492. PassphraseSalt []byte // legacy field, not used by v1 and later
  493. PassphraseHash []byte
  494. Certfps []string
  495. }
  496. // #530: support multiple client certificate fingerprints
  497. func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
  498. prefix := "account.credentials "
  499. var accounts, blobs []string
  500. tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
  501. var legacy accountCredsLegacyV8
  502. var current accountCredsLegacyV9
  503. if !strings.HasPrefix(key, prefix) {
  504. return false
  505. }
  506. account := strings.TrimPrefix(key, prefix)
  507. err := json.Unmarshal([]byte(value), &legacy)
  508. if err != nil {
  509. log.Printf("corrupt record for %s: %v\n", account, err)
  510. return true
  511. }
  512. current.Version = legacy.Version
  513. current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
  514. current.PassphraseHash = legacy.PassphraseHash
  515. if legacy.Certificate != "" {
  516. current.Certfps = []string{legacy.Certificate}
  517. }
  518. blob, err := json.Marshal(current)
  519. if err != nil {
  520. log.Printf("could not marshal record for %s: %v\n", account, err)
  521. return true
  522. }
  523. accounts = append(accounts, account)
  524. blobs = append(blobs, string(blob))
  525. return true
  526. })
  527. for i, account := range accounts {
  528. tx.Set(prefix+account, blobs[i], nil)
  529. }
  530. return nil
  531. }
  532. func init() {
  533. allChanges := []SchemaChange{
  534. {
  535. InitialVersion: "1",
  536. TargetVersion: "2",
  537. Changer: schemaChangeV1toV2,
  538. },
  539. {
  540. InitialVersion: "2",
  541. TargetVersion: "3",
  542. Changer: schemaChangeV2ToV3,
  543. },
  544. {
  545. InitialVersion: "3",
  546. TargetVersion: "4",
  547. Changer: schemaChangeV3ToV4,
  548. },
  549. {
  550. InitialVersion: "4",
  551. TargetVersion: "5",
  552. Changer: schemaChangeV4ToV5,
  553. },
  554. {
  555. InitialVersion: "5",
  556. TargetVersion: "6",
  557. Changer: schemaChangeV5ToV6,
  558. },
  559. {
  560. InitialVersion: "6",
  561. TargetVersion: "7",
  562. Changer: schemaChangeV6ToV7,
  563. },
  564. {
  565. InitialVersion: "7",
  566. TargetVersion: "8",
  567. Changer: schemaChangeV7ToV8,
  568. },
  569. {
  570. InitialVersion: "8",
  571. TargetVersion: "9",
  572. Changer: schemaChangeV8ToV9,
  573. },
  574. }
  575. // build the index
  576. schemaChanges = make(map[string]SchemaChange)
  577. for _, change := range allChanges {
  578. schemaChanges[change.InitialVersion] = change
  579. }
  580. }