2016-06-15 13:50:56 +02:00
|
|
|
// Copyright (c) 2012-2014 Jeremy Latt
|
2017-03-27 14:15:02 +02:00
|
|
|
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
2016-06-15 13:50:56 +02:00
|
|
|
// released under the MIT license
|
|
|
|
|
2014-03-02 00:02:24 +01:00
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
2020-10-02 22:48:37 +02:00
|
|
|
"encoding/base64"
|
2018-04-04 03:49:40 +02:00
|
|
|
"encoding/json"
|
2014-03-08 03:14:02 +01:00
|
|
|
"fmt"
|
2014-03-02 00:02:24 +01:00
|
|
|
"log"
|
|
|
|
"os"
|
2020-03-01 09:39:25 +01:00
|
|
|
"strconv"
|
2016-11-06 04:47:13 +01:00
|
|
|
"strings"
|
2018-04-16 22:28:31 +02:00
|
|
|
"time"
|
2016-06-15 13:50:56 +02:00
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
"github.com/oragono/oragono/irc/modes"
|
2018-04-20 09:57:48 +02:00
|
|
|
"github.com/oragono/oragono/irc/utils"
|
2017-10-05 16:03:53 +02:00
|
|
|
|
2016-09-05 10:45:42 +02:00
|
|
|
"github.com/tidwall/buntdb"
|
2014-03-02 00:02:24 +01:00
|
|
|
)
|
|
|
|
|
2016-09-05 10:45:42 +02:00
|
|
|
const (
|
2016-09-17 13:23:04 +02:00
|
|
|
// 'version' of the database schema
|
|
|
|
keySchemaVersion = "db.version"
|
2016-11-06 04:47:13 +01:00
|
|
|
// latest schema of the db
|
2020-10-02 22:48:37 +02:00
|
|
|
latestDbSchema = "14"
|
2020-05-08 07:16:49 +02:00
|
|
|
|
|
|
|
keyCloakSecret = "crypto.cloak_secret"
|
2016-09-05 10:45:42 +02:00
|
|
|
)
|
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
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
|
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
func checkDBReadyForInit(path string) error {
|
2018-12-31 07:17:44 +01:00
|
|
|
_, err := os.Stat(path)
|
|
|
|
if err == nil {
|
2020-10-02 22:48:37 +02:00
|
|
|
return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
|
2018-12-31 07:17:44 +01:00
|
|
|
} else if !os.IsNotExist(err) {
|
2020-10-02 22:48:37 +02:00
|
|
|
return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
|
2018-12-31 07:17:44 +01:00
|
|
|
}
|
2020-10-02 22:48:37 +02:00
|
|
|
return nil
|
|
|
|
}
|
2018-12-31 07:17:44 +01:00
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
// InitDB creates the database, implementing the `oragono initdb` command.
|
|
|
|
func InitDB(path string) error {
|
|
|
|
if err := checkDBReadyForInit(path); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := initializeDB(path); err != nil {
|
|
|
|
return fmt.Errorf("Could not save datastore: %w", err)
|
2018-12-31 07:17:44 +01:00
|
|
|
}
|
2020-10-02 22:48:37 +02:00
|
|
|
return nil
|
2018-12-31 07:17:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// internal database initialization code
|
|
|
|
func initializeDB(path string) error {
|
2016-09-17 13:23:04 +02:00
|
|
|
store, err := buntdb.Open(path)
|
2016-09-05 10:45:42 +02:00
|
|
|
if err != nil {
|
2018-12-31 07:17:44 +01:00
|
|
|
return err
|
2016-09-05 10:45:42 +02:00
|
|
|
}
|
|
|
|
defer store.Close()
|
|
|
|
|
|
|
|
err = store.Update(func(tx *buntdb.Tx) error {
|
2016-09-17 13:23:04 +02:00
|
|
|
// set schema version
|
2018-04-04 03:49:40 +02:00
|
|
|
tx.Set(keySchemaVersion, latestDbSchema, nil)
|
2020-05-08 07:16:49 +02:00
|
|
|
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
|
2016-09-05 10:45:42 +02:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
2018-12-31 07:17:44 +01:00
|
|
|
return err
|
2014-03-02 00:02:24 +01:00
|
|
|
}
|
|
|
|
|
2017-10-05 15:29:34 +02:00
|
|
|
// OpenDatabase returns an existing database, performing a schema version check.
|
2018-04-16 22:28:31 +02:00
|
|
|
func OpenDatabase(config *Config) (*buntdb.DB, error) {
|
2018-04-20 09:57:48 +02:00
|
|
|
return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
|
2018-04-16 22:28:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2017-09-28 07:30:53 +02:00
|
|
|
if err != nil {
|
2018-04-16 22:28:31 +02:00
|
|
|
return
|
2017-09-28 07:30:53 +02:00
|
|
|
}
|
|
|
|
|
2018-04-16 22:28:31 +02:00
|
|
|
defer func() {
|
|
|
|
if err != nil && db != nil {
|
|
|
|
db.Close()
|
|
|
|
db = nil
|
2017-09-28 07:30:53 +02:00
|
|
|
}
|
2018-04-16 22:28:31 +02:00
|
|
|
}()
|
|
|
|
|
|
|
|
// read the current version string
|
|
|
|
var version string
|
|
|
|
err = db.View(func(tx *buntdb.Tx) error {
|
|
|
|
version, err = tx.Get(keySchemaVersion)
|
|
|
|
return err
|
2017-09-28 07:30:53 +02:00
|
|
|
})
|
2018-04-16 22:28:31 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if version == latestDbSchema {
|
|
|
|
// success
|
|
|
|
return
|
|
|
|
}
|
2017-09-28 07:30:53 +02:00
|
|
|
|
2018-04-16 22:28:31 +02:00
|
|
|
// 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 {
|
2020-02-19 01:38:42 +01:00
|
|
|
err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
2018-04-16 22:28:31 +02:00
|
|
|
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)
|
2018-04-20 09:57:48 +02:00
|
|
|
err = utils.CopyFile(path, backupPath)
|
2018-04-16 22:28:31 +02:00
|
|
|
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
|
2017-09-28 07:30:53 +02:00
|
|
|
}
|
|
|
|
|
2016-09-17 13:23:04 +02:00
|
|
|
// UpgradeDB upgrades the datastore to the latest schema.
|
2018-04-16 22:28:31 +02:00
|
|
|
func UpgradeDB(config *Config) (err error) {
|
2020-02-21 10:24:18 +01:00
|
|
|
// #715: test that the database exists
|
|
|
|
_, err = os.Stat(config.Datastore.Path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
store, err := buntdb.Open(config.Datastore.Path)
|
2016-11-06 04:47:13 +01:00
|
|
|
if err != nil {
|
2018-04-16 22:28:31 +02:00
|
|
|
return err
|
2016-11-06 04:47:13 +01:00
|
|
|
}
|
|
|
|
defer store.Close()
|
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
var version string
|
2016-11-06 04:47:13 +01:00
|
|
|
err = store.Update(func(tx *buntdb.Tx) error {
|
2018-04-04 03:49:40 +02:00
|
|
|
for {
|
|
|
|
version, _ = tx.Get(keySchemaVersion)
|
|
|
|
change, schemaNeedsChange := schemaChanges[version]
|
|
|
|
if !schemaNeedsChange {
|
2018-04-16 22:28:31 +02:00
|
|
|
if version == latestDbSchema {
|
|
|
|
// success!
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// unable to upgrade to the desired version, roll back
|
2020-02-19 01:38:42 +01:00
|
|
|
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
2016-11-06 04:47:13 +01:00
|
|
|
}
|
2018-04-16 22:28:31 +02:00
|
|
|
log.Println("attempting to update schema from version " + version)
|
2018-04-04 03:49:40 +02:00
|
|
|
err := change.Changer(config, tx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-11-06 04:47:13 +01:00
|
|
|
}
|
2018-04-04 03:49:40 +02:00
|
|
|
_, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-04-16 22:28:31 +02:00
|
|
|
log.Println("successfully updated schema to version " + change.TargetVersion)
|
2016-11-06 04:47:13 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
2018-04-04 03:49:40 +02:00
|
|
|
|
2016-11-06 04:47:13 +01:00
|
|
|
if err != nil {
|
2019-01-22 02:57:39 +01:00
|
|
|
log.Printf("database upgrade failed and was rolled back: %v\n", err)
|
2016-11-06 04:47:13 +01:00
|
|
|
}
|
2018-04-16 22:28:31 +02:00
|
|
|
return err
|
2014-03-02 00:02:24 +01:00
|
|
|
}
|
2018-04-04 03:49:40 +02:00
|
|
|
|
2020-05-08 07:16:49 +02:00
|
|
|
func LoadCloakSecret(db *buntdb.DB) (result string) {
|
|
|
|
db.View(func(tx *buntdb.Tx) error {
|
|
|
|
result, _ = tx.Get(keyCloakSecret)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func StoreCloakSecret(db *buntdb.DB, secret string) {
|
|
|
|
db.Update(func(tx *buntdb.Tx) error {
|
|
|
|
tx.Set(keyCloakSecret, secret, nil)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
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
|
2018-08-28 19:34:43 +02:00
|
|
|
defaultModes := config.Channels.defaultModes
|
2018-04-04 03:49:40 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-01-22 11:01:14 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-02-06 10:32:04 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-05-19 10:27:44 +02:00
|
|
|
// custom nick enforcement was a separate db key, now it's part of settings
|
|
|
|
func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
|
|
|
|
accountToEnforcement := make(map[string]NickEnforcementMethod)
|
|
|
|
prefix := "account.customenforcement "
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
method, err := nickReservationFromString(value)
|
|
|
|
if err == nil {
|
|
|
|
accountToEnforcement[account] = method
|
|
|
|
} else {
|
|
|
|
log.Printf("skipping corrupt custom enforcement value for %s\n", account)
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
for account, method := range accountToEnforcement {
|
|
|
|
var settings AccountSettings
|
|
|
|
settings.NickEnforcement = method
|
|
|
|
text, err := json.Marshal(settings)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tx.Delete(prefix + account)
|
|
|
|
tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-10-10 10:17:44 +02:00
|
|
|
type maskInfoV7 struct {
|
|
|
|
TimeCreated time.Time
|
|
|
|
CreatorNickmask string
|
|
|
|
CreatorAccount string
|
|
|
|
}
|
|
|
|
|
|
|
|
func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
|
|
|
|
now := time.Now().UTC()
|
|
|
|
var channels []string
|
|
|
|
prefix := "channel.exists "
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
channels = append(channels, strings.TrimPrefix(key, prefix))
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
converter := func(key string) {
|
|
|
|
oldRawValue, err := tx.Get(key)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var masks []string
|
|
|
|
err = json.Unmarshal([]byte(oldRawValue), &masks)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
newCookedValue := make(map[string]maskInfoV7)
|
|
|
|
for _, mask := range masks {
|
|
|
|
normalizedMask, err := CanonicalizeMaskWildcard(mask)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
newCookedValue[normalizedMask] = maskInfoV7{
|
|
|
|
TimeCreated: now,
|
|
|
|
CreatorNickmask: "*",
|
|
|
|
CreatorAccount: "*",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newRawValue, err := json.Marshal(newCookedValue)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tx.Set(key, string(newRawValue), nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
prefixes := []string{
|
|
|
|
"channel.banlist %s",
|
|
|
|
"channel.exceptlist %s",
|
|
|
|
"channel.invitelist %s",
|
|
|
|
}
|
|
|
|
for _, channel := range channels {
|
|
|
|
for _, prefix := range prefixes {
|
|
|
|
converter(fmt.Sprintf(prefix, channel))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-12-18 23:38:14 +01:00
|
|
|
type accountSettingsLegacyV7 struct {
|
|
|
|
AutoreplayLines *int
|
|
|
|
NickEnforcement NickEnforcementMethod
|
2020-02-21 05:55:42 +01:00
|
|
|
AllowBouncer MulticlientAllowedSetting
|
2019-12-18 23:38:14 +01:00
|
|
|
AutoreplayJoins bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type accountSettingsLegacyV8 struct {
|
|
|
|
AutoreplayLines *int
|
|
|
|
NickEnforcement NickEnforcementMethod
|
2020-02-21 05:55:42 +01:00
|
|
|
AllowBouncer MulticlientAllowedSetting
|
2019-12-18 23:38:14 +01:00
|
|
|
ReplayJoins ReplayJoinsSetting
|
|
|
|
}
|
|
|
|
|
|
|
|
// #616: change autoreplay-joins to replay-joins
|
|
|
|
func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
|
|
|
|
prefix := "account.settings "
|
|
|
|
var accounts, blobs []string
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
var legacy accountSettingsLegacyV7
|
|
|
|
var current accountSettingsLegacyV8
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
err := json.Unmarshal([]byte(value), &legacy)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("corrupt record for %s: %v\n", account, err)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
current.AutoreplayLines = legacy.AutoreplayLines
|
|
|
|
current.NickEnforcement = legacy.NickEnforcement
|
|
|
|
current.AllowBouncer = legacy.AllowBouncer
|
|
|
|
if legacy.AutoreplayJoins {
|
|
|
|
current.ReplayJoins = ReplayJoinsAlways
|
|
|
|
} else {
|
|
|
|
current.ReplayJoins = ReplayJoinsCommandsOnly
|
|
|
|
}
|
|
|
|
blob, err := json.Marshal(current)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("could not marshal record for %s: %v\n", account, err)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
accounts = append(accounts, account)
|
|
|
|
blobs = append(blobs, string(blob))
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
for i, account := range accounts {
|
|
|
|
tx.Set(prefix+account, blobs[i], nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-12-29 17:59:49 +01:00
|
|
|
type accountCredsLegacyV8 struct {
|
|
|
|
Version uint
|
|
|
|
PassphraseSalt []byte // legacy field, not used by v1 and later
|
|
|
|
PassphraseHash []byte
|
|
|
|
Certificate string
|
|
|
|
}
|
|
|
|
|
|
|
|
type accountCredsLegacyV9 struct {
|
|
|
|
Version uint
|
|
|
|
PassphraseSalt []byte // legacy field, not used by v1 and later
|
|
|
|
PassphraseHash []byte
|
|
|
|
Certfps []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// #530: support multiple client certificate fingerprints
|
|
|
|
func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
|
|
|
|
prefix := "account.credentials "
|
|
|
|
var accounts, blobs []string
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
var legacy accountCredsLegacyV8
|
|
|
|
var current accountCredsLegacyV9
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
err := json.Unmarshal([]byte(value), &legacy)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("corrupt record for %s: %v\n", account, err)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
current.Version = legacy.Version
|
|
|
|
current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
|
|
|
|
current.PassphraseHash = legacy.PassphraseHash
|
|
|
|
if legacy.Certificate != "" {
|
|
|
|
current.Certfps = []string{legacy.Certificate}
|
|
|
|
}
|
|
|
|
blob, err := json.Marshal(current)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("could not marshal record for %s: %v\n", account, err)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
accounts = append(accounts, account)
|
|
|
|
blobs = append(blobs, string(blob))
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
for i, account := range accounts {
|
|
|
|
tx.Set(prefix+account, blobs[i], nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-01 09:39:25 +01:00
|
|
|
// #836: account registration time at nanosecond resolution
|
|
|
|
// (mostly to simplify testing)
|
|
|
|
func schemaChangeV9ToV10(config *Config, tx *buntdb.Tx) error {
|
|
|
|
prefix := "account.registered.time "
|
|
|
|
var accounts, times []string
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
accounts = append(accounts, account)
|
|
|
|
times = append(times, value)
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
for i, account := range accounts {
|
|
|
|
time, err := strconv.ParseInt(times[i], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("corrupt registration time entry for %s: %v\n", account, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
time = time * 1000000000
|
|
|
|
tx.Set(prefix+account, strconv.FormatInt(time, 10), nil)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-08 07:16:49 +02:00
|
|
|
// #952: move the cloak secret into the database,
|
|
|
|
// generate a new one if necessary
|
|
|
|
func schemaChangeV10ToV11(config *Config, tx *buntdb.Tx) error {
|
|
|
|
cloakSecret := config.Server.Cloaks.LegacySecretValue
|
|
|
|
if cloakSecret == "" || cloakSecret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
|
|
|
|
cloakSecret = utils.GenerateSecretKey()
|
|
|
|
}
|
|
|
|
_, _, err := tx.Set(keyCloakSecret, cloakSecret, nil)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-01 17:54:38 +02:00
|
|
|
// #1027: NickEnforcementTimeout (2) was removed,
|
|
|
|
// NickEnforcementStrict was 3 and is now 2
|
|
|
|
func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
|
|
|
|
prefix := "account.settings "
|
|
|
|
var accounts, rawSettings []string
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
accounts = append(accounts, account)
|
|
|
|
rawSettings = append(rawSettings, value)
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
for i, account := range accounts {
|
|
|
|
var settings AccountSettings
|
|
|
|
err := json.Unmarshal([]byte(rawSettings[i]), &settings)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("corrupt account settings entry for %s: %v\n", account, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// upgrade NickEnforcementTimeout (which was 2) to NickEnforcementStrict (currently 2),
|
|
|
|
// fix up the old value of NickEnforcementStrict (3) to the current value (2)
|
|
|
|
if int(settings.NickEnforcement) == 3 {
|
|
|
|
settings.NickEnforcement = NickEnforcementMethod(2)
|
|
|
|
text, err := json.Marshal(settings)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tx.Set(prefix+account, string(text), nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
type accountCredsLegacyV13 struct {
|
|
|
|
Version CredentialsVersion
|
|
|
|
PassphraseHash []byte
|
|
|
|
Certfps []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// see #212 / #284. this packs the legacy salts into a single passphrase hash,
|
|
|
|
// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`.
|
|
|
|
func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error {
|
|
|
|
salt, err := tx.Get("crypto.salt")
|
|
|
|
if err != nil {
|
|
|
|
return nil // no change required
|
|
|
|
}
|
|
|
|
tx.Delete("crypto.salt")
|
|
|
|
rawSalt, err := base64.StdEncoding.DecodeString(salt)
|
|
|
|
if err != nil {
|
|
|
|
return nil // just throw away the creds at this point
|
|
|
|
}
|
|
|
|
prefix := "account.credentials "
|
|
|
|
var accounts []string
|
|
|
|
var credentials []accountCredsLegacyV13
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
account := strings.TrimPrefix(key, prefix)
|
|
|
|
|
|
|
|
var credsOld accountCredsLegacyV9
|
|
|
|
err = json.Unmarshal([]byte(value), &credsOld)
|
|
|
|
if err != nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
// skip if these aren't legacy creds!
|
|
|
|
if credsOld.Version != 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
var credsNew accountCredsLegacyV13
|
|
|
|
credsNew.Version = 0 // mark hash for migration
|
|
|
|
credsNew.Certfps = credsOld.Certfps
|
|
|
|
credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...)
|
|
|
|
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...)
|
|
|
|
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...)
|
|
|
|
|
|
|
|
accounts = append(accounts, account)
|
|
|
|
credentials = append(credentials, credsNew)
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
for i, account := range accounts {
|
|
|
|
bytesOut, err := json.Marshal(credentials[i])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, _, err = tx.Set(prefix+account, string(bytesOut), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// channel registration time and topic set time at nanosecond resolution
|
|
|
|
func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error {
|
|
|
|
prefix := "channel.registered.time "
|
|
|
|
var channels, times []string
|
|
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
|
|
if !strings.HasPrefix(key, prefix) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
channel := strings.TrimPrefix(key, prefix)
|
|
|
|
channels = append(channels, channel)
|
|
|
|
times = append(times, value)
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
billion := int64(time.Second)
|
|
|
|
for i, channel := range channels {
|
|
|
|
regTime, err := strconv.ParseInt(times[i], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("corrupt registration time entry for %s: %v\n", channel, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
regTime = regTime * billion
|
|
|
|
tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil)
|
|
|
|
|
|
|
|
topicTimeKey := "channel.topic.settime " + channel
|
|
|
|
topicSetAt, err := tx.Get(topicTimeKey)
|
|
|
|
if err == nil {
|
|
|
|
if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil {
|
|
|
|
tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-04 03:49:40 +02:00
|
|
|
func init() {
|
|
|
|
allChanges := []SchemaChange{
|
2018-04-23 02:36:50 +02:00
|
|
|
{
|
2018-04-04 03:49:40 +02:00
|
|
|
InitialVersion: "1",
|
|
|
|
TargetVersion: "2",
|
|
|
|
Changer: schemaChangeV1toV2,
|
|
|
|
},
|
2018-04-23 02:36:50 +02:00
|
|
|
{
|
2018-04-04 03:49:40 +02:00
|
|
|
InitialVersion: "2",
|
|
|
|
TargetVersion: "3",
|
|
|
|
Changer: schemaChangeV2ToV3,
|
|
|
|
},
|
2019-01-22 11:01:14 +01:00
|
|
|
{
|
|
|
|
InitialVersion: "3",
|
|
|
|
TargetVersion: "4",
|
|
|
|
Changer: schemaChangeV3ToV4,
|
|
|
|
},
|
2019-02-06 10:32:04 +01:00
|
|
|
{
|
|
|
|
InitialVersion: "4",
|
|
|
|
TargetVersion: "5",
|
|
|
|
Changer: schemaChangeV4ToV5,
|
|
|
|
},
|
2019-05-19 10:27:44 +02:00
|
|
|
{
|
|
|
|
InitialVersion: "5",
|
|
|
|
TargetVersion: "6",
|
|
|
|
Changer: schemaChangeV5ToV6,
|
|
|
|
},
|
2019-10-10 10:17:44 +02:00
|
|
|
{
|
|
|
|
InitialVersion: "6",
|
|
|
|
TargetVersion: "7",
|
|
|
|
Changer: schemaChangeV6ToV7,
|
|
|
|
},
|
2019-12-18 23:38:14 +01:00
|
|
|
{
|
|
|
|
InitialVersion: "7",
|
|
|
|
TargetVersion: "8",
|
|
|
|
Changer: schemaChangeV7ToV8,
|
|
|
|
},
|
2019-12-29 17:59:49 +01:00
|
|
|
{
|
|
|
|
InitialVersion: "8",
|
|
|
|
TargetVersion: "9",
|
|
|
|
Changer: schemaChangeV8ToV9,
|
|
|
|
},
|
2020-03-01 09:39:25 +01:00
|
|
|
{
|
|
|
|
InitialVersion: "9",
|
|
|
|
TargetVersion: "10",
|
|
|
|
Changer: schemaChangeV9ToV10,
|
|
|
|
},
|
2020-05-08 07:16:49 +02:00
|
|
|
{
|
|
|
|
InitialVersion: "10",
|
|
|
|
TargetVersion: "11",
|
|
|
|
Changer: schemaChangeV10ToV11,
|
|
|
|
},
|
2020-06-01 17:54:38 +02:00
|
|
|
{
|
|
|
|
InitialVersion: "11",
|
|
|
|
TargetVersion: "12",
|
|
|
|
Changer: schemaChangeV11ToV12,
|
|
|
|
},
|
2020-10-02 22:48:37 +02:00
|
|
|
{
|
|
|
|
InitialVersion: "12",
|
|
|
|
TargetVersion: "13",
|
|
|
|
Changer: schemaChangeV12ToV13,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
InitialVersion: "13",
|
|
|
|
TargetVersion: "14",
|
|
|
|
Changer: schemaChangeV13ToV14,
|
|
|
|
},
|
2018-04-04 03:49:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// build the index
|
|
|
|
schemaChanges = make(map[string]SchemaChange)
|
|
|
|
for _, change := range allChanges {
|
|
|
|
schemaChanges[change.InitialVersion] = change
|
|
|
|
}
|
|
|
|
}
|