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

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