123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- // Copyright (c) 2012-2014 Jeremy Latt
- // Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "encoding/json"
- "fmt"
- "log"
- "os"
- "strings"
- "time"
-
- "github.com/oragono/oragono/irc/modes"
- "github.com/oragono/oragono/irc/utils"
-
- "github.com/tidwall/buntdb"
- )
-
- const (
- // 'version' of the database schema
- keySchemaVersion = "db.version"
- // latest schema of the db
- latestDbSchema = "5"
- )
-
- type SchemaChanger func(*Config, *buntdb.Tx) error
-
- type SchemaChange struct {
- InitialVersion string // the change will take this version
- TargetVersion string // and transform it into this version
- Changer SchemaChanger
- }
-
- // maps an initial version to a schema change capable of upgrading it
- var schemaChanges map[string]SchemaChange
-
- type incompatibleSchemaError struct {
- currentVersion string
- requiredVersion string
- }
-
- func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
- return &incompatibleSchemaError{
- currentVersion: currentVersion,
- requiredVersion: latestDbSchema,
- }
- }
-
- func (err *incompatibleSchemaError) Error() string {
- return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
- }
-
- // InitDB creates the database, implementing the `oragono initdb` command.
- func InitDB(path string) {
- _, err := os.Stat(path)
- if err == nil {
- log.Fatal("Datastore already exists (delete it manually to continue): ", path)
- } else if !os.IsNotExist(err) {
- log.Fatal("Datastore path is inaccessible: ", err.Error())
- }
-
- err = initializeDB(path)
- if err != nil {
- log.Fatal("Could not save datastore: ", err.Error())
- }
- }
-
- // internal database initialization code
- func initializeDB(path string) error {
- store, err := buntdb.Open(path)
- if err != nil {
- return err
- }
- defer store.Close()
-
- err = store.Update(func(tx *buntdb.Tx) error {
- // set schema version
- tx.Set(keySchemaVersion, latestDbSchema, nil)
- return nil
- })
-
- return err
- }
-
- // OpenDatabase returns an existing database, performing a schema version check.
- func OpenDatabase(config *Config) (*buntdb.DB, error) {
- return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
- }
-
- // open the database, giving it at most one chance to auto-upgrade the schema
- func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
- db, err = buntdb.Open(config.Datastore.Path)
- if err != nil {
- return
- }
-
- defer func() {
- if err != nil && db != nil {
- db.Close()
- db = nil
- }
- }()
-
- // read the current version string
- var version string
- err = db.View(func(tx *buntdb.Tx) error {
- version, err = tx.Get(keySchemaVersion)
- return err
- })
- if err != nil {
- return
- }
-
- if version == latestDbSchema {
- // success
- return
- }
-
- // XXX quiesce the DB so we can be sure it's safe to make a backup copy
- db.Close()
- db = nil
- if allowAutoupgrade {
- err = performAutoUpgrade(version, config)
- if err != nil {
- return
- }
- // successful autoupgrade, let's try this again:
- return openDatabaseInternal(config, false)
- } else {
- err = IncompatibleSchemaError(version)
- return
- }
- }
-
- func performAutoUpgrade(currentVersion string, config *Config) (err error) {
- path := config.Datastore.Path
- log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
- timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
- backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
- log.Printf("making a backup of current database at %s\n", backupPath)
- err = utils.CopyFile(path, backupPath)
- if err != nil {
- return err
- }
-
- err = UpgradeDB(config)
- if err != nil {
- // database upgrade is a single transaction, so we don't need to restore the backup;
- // we can just delete it
- os.Remove(backupPath)
- }
- return err
- }
-
- // UpgradeDB upgrades the datastore to the latest schema.
- func UpgradeDB(config *Config) (err error) {
- store, err := buntdb.Open(config.Datastore.Path)
- if err != nil {
- return err
- }
- defer store.Close()
-
- var version string
- err = store.Update(func(tx *buntdb.Tx) error {
- for {
- version, _ = tx.Get(keySchemaVersion)
- change, schemaNeedsChange := schemaChanges[version]
- if !schemaNeedsChange {
- if version == latestDbSchema {
- // success!
- break
- }
- // unable to upgrade to the desired version, roll back
- return IncompatibleSchemaError(version)
- }
- log.Println("attempting to update schema from version " + version)
- err := change.Changer(config, tx)
- if err != nil {
- return err
- }
- _, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil)
- if err != nil {
- return err
- }
- log.Println("successfully updated schema to version " + change.TargetVersion)
- }
- return nil
- })
-
- if err != nil {
- log.Printf("database upgrade failed and was rolled back: %v\n", err)
- }
- return err
- }
-
- func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
- // == version 1 -> 2 ==
- // account key changes and account.verified key bugfix.
-
- var keysToRemove []string
- newKeys := make(map[string]string)
-
- tx.AscendKeys("account *", func(key, value string) bool {
- keysToRemove = append(keysToRemove, key)
- splitkey := strings.Split(key, " ")
-
- // work around bug
- if splitkey[2] == "exists" {
- // manually create new verified key
- newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1])
- newKeys[newVerifiedKey] = "1"
- } else if splitkey[1] == "%s" {
- return true
- }
-
- newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1])
- newKeys[newKey] = value
-
- return true
- })
-
- for _, key := range keysToRemove {
- tx.Delete(key)
- }
- for key, value := range newKeys {
- tx.Set(key, value, nil)
- }
-
- return nil
- }
-
- // 1. channel founder names should be casefolded
- // 2. founder should be explicitly granted the ChannelFounder user mode
- // 3. explicitly initialize stored channel modes to the server default values
- func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
- var channels []string
- prefix := "channel.exists "
- tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
- if !strings.HasPrefix(key, prefix) {
- return false
- }
- chname := strings.TrimPrefix(key, prefix)
- channels = append(channels, chname)
- return true
- })
-
- // founder names should be casefolded
- // founder should be explicitly granted the ChannelFounder user mode
- for _, channel := range channels {
- founderKey := "channel.founder " + channel
- founder, _ := tx.Get(founderKey)
- if founder != "" {
- founder, err := CasefoldName(founder)
- if err == nil {
- tx.Set(founderKey, founder, nil)
- accountToUmode := map[string]modes.Mode{
- founder: modes.ChannelFounder,
- }
- atustr, _ := json.Marshal(accountToUmode)
- tx.Set("channel.accounttoumode "+channel, string(atustr), nil)
- }
- }
- }
-
- // explicitly store the channel modes
- defaultModes := config.Channels.defaultModes
- modeStrings := make([]string, len(defaultModes))
- for i, mode := range defaultModes {
- modeStrings[i] = string(mode)
- }
- defaultModeString := strings.Join(modeStrings, "")
- for _, channel := range channels {
- tx.Set("channel.modes "+channel, defaultModeString, nil)
- }
-
- return nil
- }
-
- // 1. ban info format changed (from `legacyBanInfo` below to `IPBanInfo`)
- // 2. dlines against individual IPs are normalized into dlines against the appropriate /128 network
- func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error {
- type ipRestrictTime struct {
- Duration time.Duration
- Expires time.Time
- }
- type legacyBanInfo struct {
- Reason string `json:"reason"`
- OperReason string `json:"oper_reason"`
- OperName string `json:"oper_name"`
- Time *ipRestrictTime `json:"time"`
- }
-
- now := time.Now()
- legacyToNewInfo := func(old legacyBanInfo) (new_ IPBanInfo) {
- new_.Reason = old.Reason
- new_.OperReason = old.OperReason
- new_.OperName = old.OperName
-
- if old.Time == nil {
- new_.TimeCreated = now
- new_.Duration = 0
- } else {
- new_.TimeCreated = old.Time.Expires.Add(-1 * old.Time.Duration)
- new_.Duration = old.Time.Duration
- }
- return
- }
-
- var keysToDelete []string
-
- prefix := "bans.dline "
- dlines := make(map[string]IPBanInfo)
- tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
- if !strings.HasPrefix(key, prefix) {
- return false
- }
- keysToDelete = append(keysToDelete, key)
-
- var lbinfo legacyBanInfo
- id := strings.TrimPrefix(key, prefix)
- err := json.Unmarshal([]byte(value), &lbinfo)
- if err != nil {
- log.Printf("error unmarshaling legacy dline: %v\n", err)
- return true
- }
- // legacy keys can be either an IP or a CIDR
- hostNet, err := utils.NormalizedNetFromString(id)
- if err != nil {
- log.Printf("error unmarshaling legacy dline network: %v\n", err)
- return true
- }
- dlines[utils.NetToNormalizedString(hostNet)] = legacyToNewInfo(lbinfo)
-
- return true
- })
-
- setOptions := func(info IPBanInfo) *buntdb.SetOptions {
- if info.Duration == 0 {
- return nil
- }
- ttl := info.TimeCreated.Add(info.Duration).Sub(now)
- return &buntdb.SetOptions{Expires: true, TTL: ttl}
- }
-
- // store the new dlines
- for id, info := range dlines {
- b, err := json.Marshal(info)
- if err != nil {
- log.Printf("error marshaling migrated dline: %v\n", err)
- continue
- }
- tx.Set(fmt.Sprintf("bans.dlinev2 %s", id), string(b), setOptions(info))
- }
-
- // same operations against klines
- prefix = "bans.kline "
- klines := make(map[string]IPBanInfo)
- tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
- if !strings.HasPrefix(key, prefix) {
- return false
- }
- keysToDelete = append(keysToDelete, key)
- mask := strings.TrimPrefix(key, prefix)
- var lbinfo legacyBanInfo
- err := json.Unmarshal([]byte(value), &lbinfo)
- if err != nil {
- log.Printf("error unmarshaling legacy kline: %v\n", err)
- return true
- }
- klines[mask] = legacyToNewInfo(lbinfo)
- return true
- })
-
- for mask, info := range klines {
- b, err := json.Marshal(info)
- if err != nil {
- log.Printf("error marshaling migrated kline: %v\n", err)
- continue
- }
- tx.Set(fmt.Sprintf("bans.klinev2 %s", mask), string(b), setOptions(info))
- }
-
- // clean up all the old entries
- for _, key := range keysToDelete {
- tx.Delete(key)
- }
-
- return nil
- }
-
- // create new key tracking channels that belong to an account
- func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
- founderToChannels := make(map[string][]string)
- prefix := "channel.founder "
- tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
- if !strings.HasPrefix(key, prefix) {
- return false
- }
- channel := strings.TrimPrefix(key, prefix)
- founderToChannels[value] = append(founderToChannels[value], channel)
- return true
- })
-
- for founder, channels := range founderToChannels {
- tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil)
- }
- return nil
- }
-
- func init() {
- allChanges := []SchemaChange{
- {
- InitialVersion: "1",
- TargetVersion: "2",
- Changer: schemaChangeV1toV2,
- },
- {
- InitialVersion: "2",
- TargetVersion: "3",
- Changer: schemaChangeV2ToV3,
- },
- {
- InitialVersion: "3",
- TargetVersion: "4",
- Changer: schemaChangeV3ToV4,
- },
- {
- InitialVersion: "4",
- TargetVersion: "5",
- Changer: schemaChangeV4ToV5,
- },
- }
-
- // build the index
- schemaChanges = make(map[string]SchemaChange)
- for _, change := range allChanges {
- schemaChanges[change.InitialVersion] = change
- }
- }
|