mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
8b2f6de3e0
* Add email-based password reset Fixes #734 * rename SETPASS to RESETPASS * review fixes * abuse mitigations * SENDPASS and RESETPASS should both touch the client login throttle * Produce a logline and a sno on SENDPASS (since it actually sends an email) * don't re-retrieve the settings value * add email confirmation for NS SET EMAIL * smtp: if require-tls is disabled, don't validate server cert * review fixes * remove cooldown for NS SET EMAIL If you accidentally set the wrong address, the cooldown would prevent you from fixing your mistake. Since we touch the registration throttle anyway, this shouldn't present more of an abuse concern than registration itself.
1173 lines
30 KiB
Go
1173 lines
30 KiB
Go
// Copyright (c) 2012-2014 Jeremy Latt
|
|
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
|
// released under the MIT license
|
|
|
|
package irc
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ergochat/ergo/irc/modes"
|
|
"github.com/ergochat/ergo/irc/utils"
|
|
|
|
"github.com/tidwall/buntdb"
|
|
)
|
|
|
|
const (
|
|
// 'version' of the database schema
|
|
keySchemaVersion = "db.version"
|
|
// latest schema of the db
|
|
latestDbSchema = 21
|
|
|
|
keyCloakSecret = "crypto.cloak_secret"
|
|
)
|
|
|
|
type SchemaChanger func(*Config, *buntdb.Tx) error
|
|
|
|
type SchemaChange struct {
|
|
InitialVersion int // the change will take this version
|
|
TargetVersion int // and transform it into this version
|
|
Changer SchemaChanger
|
|
}
|
|
|
|
func checkDBReadyForInit(path string) error {
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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, strconv.Itoa(latestDbSchema), nil)
|
|
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), 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 int
|
|
err = db.View(func(tx *buntdb.Tx) (err error) {
|
|
vStr, err := tx.Get(keySchemaVersion)
|
|
if err == nil {
|
|
version, err = strconv.Atoi(vStr)
|
|
}
|
|
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 = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
|
return
|
|
}
|
|
}
|
|
|
|
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
|
path := config.Datastore.Path
|
|
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
|
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
|
backupPath := fmt.Sprintf("%s.v%d.%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) {
|
|
// #715: test that the database exists
|
|
_, err = os.Stat(config.Datastore.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
store, err := buntdb.Open(config.Datastore.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer store.Close()
|
|
|
|
var version int
|
|
err = store.Update(func(tx *buntdb.Tx) error {
|
|
for {
|
|
vStr, _ := tx.Get(keySchemaVersion)
|
|
version, _ = strconv.Atoi(vStr)
|
|
if version == latestDbSchema {
|
|
// success!
|
|
break
|
|
}
|
|
change, ok := getSchemaChange(version)
|
|
if !ok {
|
|
// unable to upgrade to the desired version, roll back
|
|
return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
|
|
}
|
|
log.Printf("attempting to update schema from version %d\n", version)
|
|
err := change.Changer(config, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
log.Printf("database upgrade failed and was rolled back: %v\n", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type accountSettingsLegacyV7 struct {
|
|
AutoreplayLines *int
|
|
NickEnforcement NickEnforcementMethod
|
|
AllowBouncer MulticlientAllowedSetting
|
|
AutoreplayJoins bool
|
|
}
|
|
|
|
type accountSettingsLegacyV8 struct {
|
|
AutoreplayLines *int
|
|
NickEnforcement NickEnforcementMethod
|
|
AllowBouncer MulticlientAllowedSetting
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// #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
|
|
}
|
|
|
|
// #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
|
|
}
|
|
|
|
// #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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// #1327: delete any invalid klines
|
|
func schemaChangeV14ToV15(config *Config, tx *buntdb.Tx) error {
|
|
prefix := "bans.klinev2 "
|
|
var keys []string
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return false
|
|
}
|
|
if key != strings.TrimSpace(key) {
|
|
keys = append(keys, key)
|
|
}
|
|
return true
|
|
})
|
|
// don't bother trying to fix these up
|
|
for _, key := range keys {
|
|
tx.Delete(key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// #1330: delete any stale realname records
|
|
func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error {
|
|
prefix := "account.realname "
|
|
verifiedPrefix := "account.verified "
|
|
var keys []string
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return false
|
|
}
|
|
acct := strings.TrimPrefix(key, prefix)
|
|
verifiedKey := verifiedPrefix + acct
|
|
_, verifiedErr := tx.Get(verifiedKey)
|
|
if verifiedErr != nil {
|
|
keys = append(keys, key)
|
|
}
|
|
return true
|
|
})
|
|
for _, key := range keys {
|
|
tx.Delete(key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// #1346: remove vhost request queue
|
|
func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
|
|
prefix := "vhostQueue "
|
|
var keys []string
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return false
|
|
}
|
|
keys = append(keys, key)
|
|
return true
|
|
})
|
|
|
|
for _, key := range keys {
|
|
tx.Delete(key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// #1274: we used to suspend accounts by deleting their "verified" key,
|
|
// now we save some metadata under a new key
|
|
func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error {
|
|
now := time.Now().UTC()
|
|
|
|
exists := "account.exists "
|
|
suspended := "account.suspended "
|
|
verif := "account.verified "
|
|
verifCode := "account.verificationcode "
|
|
|
|
var accounts []string
|
|
|
|
tx.AscendGreaterOrEqual("", exists, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, exists) {
|
|
return false
|
|
}
|
|
account := strings.TrimPrefix(key, exists)
|
|
_, verifiedErr := tx.Get(verif + account)
|
|
_, verifCodeErr := tx.Get(verifCode + account)
|
|
if verifiedErr != nil && verifCodeErr != nil {
|
|
// verified key not present, but there's no code either,
|
|
// this is a suspension
|
|
accounts = append(accounts, account)
|
|
}
|
|
return true
|
|
})
|
|
|
|
type accountSuspensionV18 struct {
|
|
TimeCreated time.Time
|
|
Duration time.Duration
|
|
OperName string
|
|
Reason string
|
|
}
|
|
|
|
for _, account := range accounts {
|
|
var sus accountSuspensionV18
|
|
sus.TimeCreated = now
|
|
sus.OperName = "*"
|
|
sus.Reason = "[unknown]"
|
|
susBytes, err := json.Marshal(sus)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx.Set(suspended+account, string(susBytes), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// #1345: persist the channel-user modes of always-on clients
|
|
func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error {
|
|
channelToAmodesCache := make(map[string]map[string]modes.Mode)
|
|
joinedto := "account.joinedto "
|
|
var accounts []string
|
|
var channels [][]string
|
|
tx.AscendGreaterOrEqual("", joinedto, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, joinedto) {
|
|
return false
|
|
}
|
|
accounts = append(accounts, strings.TrimPrefix(key, joinedto))
|
|
var ch []string
|
|
if value != "" {
|
|
ch = strings.Split(value, ",")
|
|
}
|
|
channels = append(channels, ch)
|
|
return true
|
|
})
|
|
|
|
for i := 0; i < len(accounts); i++ {
|
|
account := accounts[i]
|
|
channels := channels[i]
|
|
tx.Delete(joinedto + account)
|
|
newValue := make(map[string]string, len(channels))
|
|
for _, channel := range channels {
|
|
chcfname, err := CasefoldChannel(channel)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
// get amodes from the channelToAmodesCache, fill if necessary
|
|
amodes, ok := channelToAmodesCache[chcfname]
|
|
if !ok {
|
|
amodeStr, _ := tx.Get("channel.accounttoumode " + chcfname)
|
|
if amodeStr != "" {
|
|
jErr := json.Unmarshal([]byte(amodeStr), &amodes)
|
|
if jErr != nil {
|
|
log.Printf("error retrieving amodes for %s: %v\n", channel, jErr)
|
|
amodes = nil
|
|
}
|
|
}
|
|
// setting/using the nil value here is ok
|
|
channelToAmodesCache[chcfname] = amodes
|
|
}
|
|
if mode, ok := amodes[account]; ok {
|
|
newValue[channel] = string(mode)
|
|
} else {
|
|
newValue[channel] = ""
|
|
}
|
|
}
|
|
newValueBytes, jErr := json.Marshal(newValue)
|
|
if jErr != nil {
|
|
log.Printf("couldn't serialize new mode values for v19: %v\n", jErr)
|
|
continue
|
|
}
|
|
tx.Set("account.channeltomodes "+account, string(newValueBytes), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// #1490: start tracking join times for always-on clients
|
|
func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
|
|
type joinData struct {
|
|
Modes string
|
|
JoinTime int64
|
|
}
|
|
|
|
var accounts []string
|
|
var data []string
|
|
|
|
now := time.Now().UnixNano()
|
|
|
|
prefix := "account.channeltomodes "
|
|
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
return false
|
|
}
|
|
accounts = append(accounts, strings.TrimPrefix(key, prefix))
|
|
data = append(data, value)
|
|
return true
|
|
})
|
|
|
|
for i, account := range accounts {
|
|
var existingMap map[string]string
|
|
err := json.Unmarshal([]byte(data[i]), &existingMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newMap := make(map[string]joinData)
|
|
for channel, modeStr := range existingMap {
|
|
newMap[channel] = joinData{
|
|
Modes: modeStr,
|
|
JoinTime: now,
|
|
}
|
|
}
|
|
serialized, err := json.Marshal(newMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx.Set(prefix+account, string(serialized), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// #734: move the email address into the settings object,
|
|
// giving people a way to change it
|
|
func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error {
|
|
type accountSettingsv21 struct {
|
|
AutoreplayLines *int
|
|
NickEnforcement NickEnforcementMethod
|
|
AllowBouncer MulticlientAllowedSetting
|
|
ReplayJoins ReplayJoinsSetting
|
|
AlwaysOn PersistentStatus
|
|
AutoreplayMissed bool
|
|
DMHistory HistoryStatus
|
|
AutoAway PersistentStatus
|
|
Email string
|
|
}
|
|
var accounts []string
|
|
var emails []string
|
|
callbackPrefix := "account.callback "
|
|
tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, callbackPrefix) {
|
|
return false
|
|
}
|
|
account := strings.TrimPrefix(key, callbackPrefix)
|
|
if _, err := tx.Get("account.verified " + account); err != nil {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(value, "mailto:") {
|
|
accounts = append(accounts, account)
|
|
emails = append(emails, strings.TrimPrefix(value, "mailto:"))
|
|
}
|
|
return true
|
|
})
|
|
for i, account := range accounts {
|
|
var settings accountSettingsv21
|
|
email := emails[i]
|
|
settingsKey := "account.settings " + account
|
|
settingsStr, err := tx.Get(settingsKey)
|
|
if err == nil && settingsStr != "" {
|
|
json.Unmarshal([]byte(settingsStr), &settings)
|
|
}
|
|
settings.Email = email
|
|
settingsBytes, err := json.Marshal(settings)
|
|
if err != nil {
|
|
log.Printf("couldn't marshal settings for %s: %v\n", account, err)
|
|
} else {
|
|
tx.Set(settingsKey, string(settingsBytes), nil)
|
|
}
|
|
tx.Delete(callbackPrefix + account)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
|
for _, change := range allChanges {
|
|
if initialVersion == change.InitialVersion {
|
|
return change, true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
var 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,
|
|
},
|
|
{
|
|
InitialVersion: 5,
|
|
TargetVersion: 6,
|
|
Changer: schemaChangeV5ToV6,
|
|
},
|
|
{
|
|
InitialVersion: 6,
|
|
TargetVersion: 7,
|
|
Changer: schemaChangeV6ToV7,
|
|
},
|
|
{
|
|
InitialVersion: 7,
|
|
TargetVersion: 8,
|
|
Changer: schemaChangeV7ToV8,
|
|
},
|
|
{
|
|
InitialVersion: 8,
|
|
TargetVersion: 9,
|
|
Changer: schemaChangeV8ToV9,
|
|
},
|
|
{
|
|
InitialVersion: 9,
|
|
TargetVersion: 10,
|
|
Changer: schemaChangeV9ToV10,
|
|
},
|
|
{
|
|
InitialVersion: 10,
|
|
TargetVersion: 11,
|
|
Changer: schemaChangeV10ToV11,
|
|
},
|
|
{
|
|
InitialVersion: 11,
|
|
TargetVersion: 12,
|
|
Changer: schemaChangeV11ToV12,
|
|
},
|
|
{
|
|
InitialVersion: 12,
|
|
TargetVersion: 13,
|
|
Changer: schemaChangeV12ToV13,
|
|
},
|
|
{
|
|
InitialVersion: 13,
|
|
TargetVersion: 14,
|
|
Changer: schemaChangeV13ToV14,
|
|
},
|
|
{
|
|
InitialVersion: 14,
|
|
TargetVersion: 15,
|
|
Changer: schemaChangeV14ToV15,
|
|
},
|
|
{
|
|
InitialVersion: 15,
|
|
TargetVersion: 16,
|
|
Changer: schemaChangeV15ToV16,
|
|
},
|
|
{
|
|
InitialVersion: 16,
|
|
TargetVersion: 17,
|
|
Changer: schemaChangeV16ToV17,
|
|
},
|
|
{
|
|
InitialVersion: 17,
|
|
TargetVersion: 18,
|
|
Changer: schemaChangeV17ToV18,
|
|
},
|
|
{
|
|
InitialVersion: 18,
|
|
TargetVersion: 19,
|
|
Changer: schemaChangeV18To19,
|
|
},
|
|
{
|
|
InitialVersion: 19,
|
|
TargetVersion: 20,
|
|
Changer: schemaChangeV19To20,
|
|
},
|
|
{
|
|
InitialVersion: 20,
|
|
TargetVersion: 21,
|
|
Changer: schemaChangeV20To21,
|
|
},
|
|
}
|