1// Copyright (c) 2012-2014 Jeremy Latt 2// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net> 3// released under the MIT license 4 5package irc 6 7import ( 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "log" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/ergochat/ergo/irc/modes" 18 "github.com/ergochat/ergo/irc/utils" 19 20 "github.com/tidwall/buntdb" 21) 22 23const ( 24 // 'version' of the database schema 25 keySchemaVersion = "db.version" 26 // latest schema of the db 27 latestDbSchema = 22 28 29 keyCloakSecret = "crypto.cloak_secret" 30) 31 32type SchemaChanger func(*Config, *buntdb.Tx) error 33 34type SchemaChange struct { 35 InitialVersion int // the change will take this version 36 TargetVersion int // and transform it into this version 37 Changer SchemaChanger 38} 39 40func checkDBReadyForInit(path string) error { 41 _, err := os.Stat(path) 42 if err == nil { 43 return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path) 44 } else if !os.IsNotExist(err) { 45 return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err) 46 } 47 return nil 48} 49 50// InitDB creates the database, implementing the `oragono initdb` command. 51func InitDB(path string) error { 52 if err := checkDBReadyForInit(path); err != nil { 53 return err 54 } 55 56 if err := initializeDB(path); err != nil { 57 return fmt.Errorf("Could not save datastore: %w", err) 58 } 59 return nil 60} 61 62// internal database initialization code 63func initializeDB(path string) error { 64 store, err := buntdb.Open(path) 65 if err != nil { 66 return err 67 } 68 defer store.Close() 69 70 err = store.Update(func(tx *buntdb.Tx) error { 71 // set schema version 72 tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil) 73 tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) 74 return nil 75 }) 76 77 return err 78} 79 80// OpenDatabase returns an existing database, performing a schema version check. 81func OpenDatabase(config *Config) (*buntdb.DB, error) { 82 return openDatabaseInternal(config, config.Datastore.AutoUpgrade) 83} 84 85// open the database, giving it at most one chance to auto-upgrade the schema 86func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) { 87 db, err = buntdb.Open(config.Datastore.Path) 88 if err != nil { 89 return 90 } 91 92 defer func() { 93 if err != nil && db != nil { 94 db.Close() 95 db = nil 96 } 97 }() 98 99 // read the current version string 100 var version int 101 err = db.View(func(tx *buntdb.Tx) (err error) { 102 vStr, err := tx.Get(keySchemaVersion) 103 if err == nil { 104 version, err = strconv.Atoi(vStr) 105 } 106 return err 107 }) 108 if err != nil { 109 return 110 } 111 112 if version == latestDbSchema { 113 // success 114 return 115 } 116 117 // XXX quiesce the DB so we can be sure it's safe to make a backup copy 118 db.Close() 119 db = nil 120 if allowAutoupgrade { 121 err = performAutoUpgrade(version, config) 122 if err != nil { 123 return 124 } 125 // successful autoupgrade, let's try this again: 126 return openDatabaseInternal(config, false) 127 } else { 128 err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema} 129 return 130 } 131} 132 133func performAutoUpgrade(currentVersion int, config *Config) (err error) { 134 path := config.Datastore.Path 135 log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema) 136 timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z") 137 backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp) 138 log.Printf("making a backup of current database at %s\n", backupPath) 139 err = utils.CopyFile(path, backupPath) 140 if err != nil { 141 return err 142 } 143 144 err = UpgradeDB(config) 145 if err != nil { 146 // database upgrade is a single transaction, so we don't need to restore the backup; 147 // we can just delete it 148 os.Remove(backupPath) 149 } 150 return err 151} 152 153// UpgradeDB upgrades the datastore to the latest schema. 154func UpgradeDB(config *Config) (err error) { 155 // #715: test that the database exists 156 _, err = os.Stat(config.Datastore.Path) 157 if err != nil { 158 return err 159 } 160 161 store, err := buntdb.Open(config.Datastore.Path) 162 if err != nil { 163 return err 164 } 165 defer store.Close() 166 167 var version int 168 err = store.Update(func(tx *buntdb.Tx) error { 169 for { 170 vStr, _ := tx.Get(keySchemaVersion) 171 version, _ = strconv.Atoi(vStr) 172 if version == latestDbSchema { 173 // success! 174 break 175 } 176 change, ok := getSchemaChange(version) 177 if !ok { 178 // unable to upgrade to the desired version, roll back 179 return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema} 180 } 181 log.Printf("attempting to update schema from version %d\n", version) 182 err := change.Changer(config, tx) 183 if err != nil { 184 return err 185 } 186 _, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil) 187 if err != nil { 188 return err 189 } 190 log.Printf("successfully updated schema to version %d\n", change.TargetVersion) 191 } 192 return nil 193 }) 194 195 if err != nil { 196 log.Printf("database upgrade failed and was rolled back: %v\n", err) 197 } 198 return err 199} 200 201func LoadCloakSecret(db *buntdb.DB) (result string) { 202 db.View(func(tx *buntdb.Tx) error { 203 result, _ = tx.Get(keyCloakSecret) 204 return nil 205 }) 206 return 207} 208 209func StoreCloakSecret(db *buntdb.DB, secret string) { 210 db.Update(func(tx *buntdb.Tx) error { 211 tx.Set(keyCloakSecret, secret, nil) 212 return nil 213 }) 214} 215 216func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error { 217 // == version 1 -> 2 == 218 // account key changes and account.verified key bugfix. 219 220 var keysToRemove []string 221 newKeys := make(map[string]string) 222 223 tx.AscendKeys("account *", func(key, value string) bool { 224 keysToRemove = append(keysToRemove, key) 225 splitkey := strings.Split(key, " ") 226 227 // work around bug 228 if splitkey[2] == "exists" { 229 // manually create new verified key 230 newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1]) 231 newKeys[newVerifiedKey] = "1" 232 } else if splitkey[1] == "%s" { 233 return true 234 } 235 236 newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1]) 237 newKeys[newKey] = value 238 239 return true 240 }) 241 242 for _, key := range keysToRemove { 243 tx.Delete(key) 244 } 245 for key, value := range newKeys { 246 tx.Set(key, value, nil) 247 } 248 249 return nil 250} 251 252// 1. channel founder names should be casefolded 253// 2. founder should be explicitly granted the ChannelFounder user mode 254// 3. explicitly initialize stored channel modes to the server default values 255func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error { 256 var channels []string 257 prefix := "channel.exists " 258 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 259 if !strings.HasPrefix(key, prefix) { 260 return false 261 } 262 chname := strings.TrimPrefix(key, prefix) 263 channels = append(channels, chname) 264 return true 265 }) 266 267 // founder names should be casefolded 268 // founder should be explicitly granted the ChannelFounder user mode 269 for _, channel := range channels { 270 founderKey := "channel.founder " + channel 271 founder, _ := tx.Get(founderKey) 272 if founder != "" { 273 founder, err := CasefoldName(founder) 274 if err == nil { 275 tx.Set(founderKey, founder, nil) 276 accountToUmode := map[string]modes.Mode{ 277 founder: modes.ChannelFounder, 278 } 279 atustr, _ := json.Marshal(accountToUmode) 280 tx.Set("channel.accounttoumode "+channel, string(atustr), nil) 281 } 282 } 283 } 284 285 // explicitly store the channel modes 286 defaultModes := config.Channels.defaultModes 287 modeStrings := make([]string, len(defaultModes)) 288 for i, mode := range defaultModes { 289 modeStrings[i] = string(mode) 290 } 291 defaultModeString := strings.Join(modeStrings, "") 292 for _, channel := range channels { 293 tx.Set("channel.modes "+channel, defaultModeString, nil) 294 } 295 296 return nil 297} 298 299// 1. ban info format changed (from `legacyBanInfo` below to `IPBanInfo`) 300// 2. dlines against individual IPs are normalized into dlines against the appropriate /128 network 301func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error { 302 type ipRestrictTime struct { 303 Duration time.Duration 304 Expires time.Time 305 } 306 type legacyBanInfo struct { 307 Reason string `json:"reason"` 308 OperReason string `json:"oper_reason"` 309 OperName string `json:"oper_name"` 310 Time *ipRestrictTime `json:"time"` 311 } 312 313 now := time.Now() 314 legacyToNewInfo := func(old legacyBanInfo) (new_ IPBanInfo) { 315 new_.Reason = old.Reason 316 new_.OperReason = old.OperReason 317 new_.OperName = old.OperName 318 319 if old.Time == nil { 320 new_.TimeCreated = now 321 new_.Duration = 0 322 } else { 323 new_.TimeCreated = old.Time.Expires.Add(-1 * old.Time.Duration) 324 new_.Duration = old.Time.Duration 325 } 326 return 327 } 328 329 var keysToDelete []string 330 331 prefix := "bans.dline " 332 dlines := make(map[string]IPBanInfo) 333 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 334 if !strings.HasPrefix(key, prefix) { 335 return false 336 } 337 keysToDelete = append(keysToDelete, key) 338 339 var lbinfo legacyBanInfo 340 id := strings.TrimPrefix(key, prefix) 341 err := json.Unmarshal([]byte(value), &lbinfo) 342 if err != nil { 343 log.Printf("error unmarshaling legacy dline: %v\n", err) 344 return true 345 } 346 // legacy keys can be either an IP or a CIDR 347 hostNet, err := utils.NormalizedNetFromString(id) 348 if err != nil { 349 log.Printf("error unmarshaling legacy dline network: %v\n", err) 350 return true 351 } 352 dlines[utils.NetToNormalizedString(hostNet)] = legacyToNewInfo(lbinfo) 353 354 return true 355 }) 356 357 setOptions := func(info IPBanInfo) *buntdb.SetOptions { 358 if info.Duration == 0 { 359 return nil 360 } 361 ttl := info.TimeCreated.Add(info.Duration).Sub(now) 362 return &buntdb.SetOptions{Expires: true, TTL: ttl} 363 } 364 365 // store the new dlines 366 for id, info := range dlines { 367 b, err := json.Marshal(info) 368 if err != nil { 369 log.Printf("error marshaling migrated dline: %v\n", err) 370 continue 371 } 372 tx.Set(fmt.Sprintf("bans.dlinev2 %s", id), string(b), setOptions(info)) 373 } 374 375 // same operations against klines 376 prefix = "bans.kline " 377 klines := make(map[string]IPBanInfo) 378 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 379 if !strings.HasPrefix(key, prefix) { 380 return false 381 } 382 keysToDelete = append(keysToDelete, key) 383 mask := strings.TrimPrefix(key, prefix) 384 var lbinfo legacyBanInfo 385 err := json.Unmarshal([]byte(value), &lbinfo) 386 if err != nil { 387 log.Printf("error unmarshaling legacy kline: %v\n", err) 388 return true 389 } 390 klines[mask] = legacyToNewInfo(lbinfo) 391 return true 392 }) 393 394 for mask, info := range klines { 395 b, err := json.Marshal(info) 396 if err != nil { 397 log.Printf("error marshaling migrated kline: %v\n", err) 398 continue 399 } 400 tx.Set(fmt.Sprintf("bans.klinev2 %s", mask), string(b), setOptions(info)) 401 } 402 403 // clean up all the old entries 404 for _, key := range keysToDelete { 405 tx.Delete(key) 406 } 407 408 return nil 409} 410 411// create new key tracking channels that belong to an account 412func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error { 413 founderToChannels := make(map[string][]string) 414 prefix := "channel.founder " 415 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 416 if !strings.HasPrefix(key, prefix) { 417 return false 418 } 419 channel := strings.TrimPrefix(key, prefix) 420 founderToChannels[value] = append(founderToChannels[value], channel) 421 return true 422 }) 423 424 for founder, channels := range founderToChannels { 425 tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil) 426 } 427 return nil 428} 429 430// custom nick enforcement was a separate db key, now it's part of settings 431func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error { 432 accountToEnforcement := make(map[string]NickEnforcementMethod) 433 prefix := "account.customenforcement " 434 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 435 if !strings.HasPrefix(key, prefix) { 436 return false 437 } 438 account := strings.TrimPrefix(key, prefix) 439 method, err := nickReservationFromString(value) 440 if err == nil { 441 accountToEnforcement[account] = method 442 } else { 443 log.Printf("skipping corrupt custom enforcement value for %s\n", account) 444 } 445 return true 446 }) 447 448 for account, method := range accountToEnforcement { 449 var settings AccountSettings 450 settings.NickEnforcement = method 451 text, err := json.Marshal(settings) 452 if err != nil { 453 return err 454 } 455 tx.Delete(prefix + account) 456 tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil) 457 } 458 return nil 459} 460 461type maskInfoV7 struct { 462 TimeCreated time.Time 463 CreatorNickmask string 464 CreatorAccount string 465} 466 467func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error { 468 now := time.Now().UTC() 469 var channels []string 470 prefix := "channel.exists " 471 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 472 if !strings.HasPrefix(key, prefix) { 473 return false 474 } 475 channels = append(channels, strings.TrimPrefix(key, prefix)) 476 return true 477 }) 478 479 converter := func(key string) { 480 oldRawValue, err := tx.Get(key) 481 if err != nil { 482 return 483 } 484 var masks []string 485 err = json.Unmarshal([]byte(oldRawValue), &masks) 486 if err != nil { 487 return 488 } 489 newCookedValue := make(map[string]maskInfoV7) 490 for _, mask := range masks { 491 normalizedMask, err := CanonicalizeMaskWildcard(mask) 492 if err != nil { 493 continue 494 } 495 newCookedValue[normalizedMask] = maskInfoV7{ 496 TimeCreated: now, 497 CreatorNickmask: "*", 498 CreatorAccount: "*", 499 } 500 } 501 newRawValue, err := json.Marshal(newCookedValue) 502 if err != nil { 503 return 504 } 505 tx.Set(key, string(newRawValue), nil) 506 } 507 508 prefixes := []string{ 509 "channel.banlist %s", 510 "channel.exceptlist %s", 511 "channel.invitelist %s", 512 } 513 for _, channel := range channels { 514 for _, prefix := range prefixes { 515 converter(fmt.Sprintf(prefix, channel)) 516 } 517 } 518 return nil 519} 520 521type accountSettingsLegacyV7 struct { 522 AutoreplayLines *int 523 NickEnforcement NickEnforcementMethod 524 AllowBouncer MulticlientAllowedSetting 525 AutoreplayJoins bool 526} 527 528type accountSettingsLegacyV8 struct { 529 AutoreplayLines *int 530 NickEnforcement NickEnforcementMethod 531 AllowBouncer MulticlientAllowedSetting 532 ReplayJoins ReplayJoinsSetting 533} 534 535// #616: change autoreplay-joins to replay-joins 536func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error { 537 prefix := "account.settings " 538 var accounts, blobs []string 539 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 540 var legacy accountSettingsLegacyV7 541 var current accountSettingsLegacyV8 542 if !strings.HasPrefix(key, prefix) { 543 return false 544 } 545 account := strings.TrimPrefix(key, prefix) 546 err := json.Unmarshal([]byte(value), &legacy) 547 if err != nil { 548 log.Printf("corrupt record for %s: %v\n", account, err) 549 return true 550 } 551 current.AutoreplayLines = legacy.AutoreplayLines 552 current.NickEnforcement = legacy.NickEnforcement 553 current.AllowBouncer = legacy.AllowBouncer 554 if legacy.AutoreplayJoins { 555 current.ReplayJoins = ReplayJoinsAlways 556 } else { 557 current.ReplayJoins = ReplayJoinsCommandsOnly 558 } 559 blob, err := json.Marshal(current) 560 if err != nil { 561 log.Printf("could not marshal record for %s: %v\n", account, err) 562 return true 563 } 564 accounts = append(accounts, account) 565 blobs = append(blobs, string(blob)) 566 return true 567 }) 568 for i, account := range accounts { 569 tx.Set(prefix+account, blobs[i], nil) 570 } 571 return nil 572} 573 574type accountCredsLegacyV8 struct { 575 Version uint 576 PassphraseSalt []byte // legacy field, not used by v1 and later 577 PassphraseHash []byte 578 Certificate string 579} 580 581type accountCredsLegacyV9 struct { 582 Version uint 583 PassphraseSalt []byte // legacy field, not used by v1 and later 584 PassphraseHash []byte 585 Certfps []string 586} 587 588// #530: support multiple client certificate fingerprints 589func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error { 590 prefix := "account.credentials " 591 var accounts, blobs []string 592 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 593 var legacy accountCredsLegacyV8 594 var current accountCredsLegacyV9 595 if !strings.HasPrefix(key, prefix) { 596 return false 597 } 598 account := strings.TrimPrefix(key, prefix) 599 err := json.Unmarshal([]byte(value), &legacy) 600 if err != nil { 601 log.Printf("corrupt record for %s: %v\n", account, err) 602 return true 603 } 604 current.Version = legacy.Version 605 current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this 606 current.PassphraseHash = legacy.PassphraseHash 607 if legacy.Certificate != "" { 608 current.Certfps = []string{legacy.Certificate} 609 } 610 blob, err := json.Marshal(current) 611 if err != nil { 612 log.Printf("could not marshal record for %s: %v\n", account, err) 613 return true 614 } 615 accounts = append(accounts, account) 616 blobs = append(blobs, string(blob)) 617 return true 618 }) 619 for i, account := range accounts { 620 tx.Set(prefix+account, blobs[i], nil) 621 } 622 return nil 623} 624 625// #836: account registration time at nanosecond resolution 626// (mostly to simplify testing) 627func schemaChangeV9ToV10(config *Config, tx *buntdb.Tx) error { 628 prefix := "account.registered.time " 629 var accounts, times []string 630 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 631 if !strings.HasPrefix(key, prefix) { 632 return false 633 } 634 account := strings.TrimPrefix(key, prefix) 635 accounts = append(accounts, account) 636 times = append(times, value) 637 return true 638 }) 639 for i, account := range accounts { 640 time, err := strconv.ParseInt(times[i], 10, 64) 641 if err != nil { 642 log.Printf("corrupt registration time entry for %s: %v\n", account, err) 643 continue 644 } 645 time = time * 1000000000 646 tx.Set(prefix+account, strconv.FormatInt(time, 10), nil) 647 } 648 return nil 649} 650 651// #952: move the cloak secret into the database, 652// generate a new one if necessary 653func schemaChangeV10ToV11(config *Config, tx *buntdb.Tx) error { 654 cloakSecret := config.Server.Cloaks.LegacySecretValue 655 if cloakSecret == "" || cloakSecret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" { 656 cloakSecret = utils.GenerateSecretKey() 657 } 658 _, _, err := tx.Set(keyCloakSecret, cloakSecret, nil) 659 return err 660} 661 662// #1027: NickEnforcementTimeout (2) was removed, 663// NickEnforcementStrict was 3 and is now 2 664func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error { 665 prefix := "account.settings " 666 var accounts, rawSettings []string 667 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 668 if !strings.HasPrefix(key, prefix) { 669 return false 670 } 671 account := strings.TrimPrefix(key, prefix) 672 accounts = append(accounts, account) 673 rawSettings = append(rawSettings, value) 674 return true 675 }) 676 677 for i, account := range accounts { 678 var settings AccountSettings 679 err := json.Unmarshal([]byte(rawSettings[i]), &settings) 680 if err != nil { 681 log.Printf("corrupt account settings entry for %s: %v\n", account, err) 682 continue 683 } 684 // upgrade NickEnforcementTimeout (which was 2) to NickEnforcementStrict (currently 2), 685 // fix up the old value of NickEnforcementStrict (3) to the current value (2) 686 if int(settings.NickEnforcement) == 3 { 687 settings.NickEnforcement = NickEnforcementMethod(2) 688 text, err := json.Marshal(settings) 689 if err != nil { 690 return err 691 } 692 tx.Set(prefix+account, string(text), nil) 693 } 694 } 695 return nil 696} 697 698type accountCredsLegacyV13 struct { 699 Version CredentialsVersion 700 PassphraseHash []byte 701 Certfps []string 702} 703 704// see #212 / #284. this packs the legacy salts into a single passphrase hash, 705// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`. 706func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error { 707 salt, err := tx.Get("crypto.salt") 708 if err != nil { 709 return nil // no change required 710 } 711 tx.Delete("crypto.salt") 712 rawSalt, err := base64.StdEncoding.DecodeString(salt) 713 if err != nil { 714 return nil // just throw away the creds at this point 715 } 716 prefix := "account.credentials " 717 var accounts []string 718 var credentials []accountCredsLegacyV13 719 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 720 if !strings.HasPrefix(key, prefix) { 721 return false 722 } 723 account := strings.TrimPrefix(key, prefix) 724 725 var credsOld accountCredsLegacyV9 726 err = json.Unmarshal([]byte(value), &credsOld) 727 if err != nil { 728 return true 729 } 730 // skip if these aren't legacy creds! 731 if credsOld.Version != 0 { 732 return true 733 } 734 735 var credsNew accountCredsLegacyV13 736 credsNew.Version = 0 // mark hash for migration 737 credsNew.Certfps = credsOld.Certfps 738 credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...) 739 credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...) 740 credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...) 741 742 accounts = append(accounts, account) 743 credentials = append(credentials, credsNew) 744 return true 745 }) 746 747 for i, account := range accounts { 748 bytesOut, err := json.Marshal(credentials[i]) 749 if err != nil { 750 return err 751 } 752 _, _, err = tx.Set(prefix+account, string(bytesOut), nil) 753 if err != nil { 754 return err 755 } 756 } 757 758 return nil 759} 760 761// channel registration time and topic set time at nanosecond resolution 762func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error { 763 prefix := "channel.registered.time " 764 var channels, times []string 765 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 766 if !strings.HasPrefix(key, prefix) { 767 return false 768 } 769 channel := strings.TrimPrefix(key, prefix) 770 channels = append(channels, channel) 771 times = append(times, value) 772 return true 773 }) 774 775 billion := int64(time.Second) 776 for i, channel := range channels { 777 regTime, err := strconv.ParseInt(times[i], 10, 64) 778 if err != nil { 779 log.Printf("corrupt registration time entry for %s: %v\n", channel, err) 780 continue 781 } 782 regTime = regTime * billion 783 tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil) 784 785 topicTimeKey := "channel.topic.settime " + channel 786 topicSetAt, err := tx.Get(topicTimeKey) 787 if err == nil { 788 if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil { 789 tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil) 790 } 791 } 792 } 793 return nil 794} 795 796// #1327: delete any invalid klines 797func schemaChangeV14ToV15(config *Config, tx *buntdb.Tx) error { 798 prefix := "bans.klinev2 " 799 var keys []string 800 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 801 if !strings.HasPrefix(key, prefix) { 802 return false 803 } 804 if key != strings.TrimSpace(key) { 805 keys = append(keys, key) 806 } 807 return true 808 }) 809 // don't bother trying to fix these up 810 for _, key := range keys { 811 tx.Delete(key) 812 } 813 return nil 814} 815 816// #1330: delete any stale realname records 817func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error { 818 prefix := "account.realname " 819 verifiedPrefix := "account.verified " 820 var keys []string 821 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 822 if !strings.HasPrefix(key, prefix) { 823 return false 824 } 825 acct := strings.TrimPrefix(key, prefix) 826 verifiedKey := verifiedPrefix + acct 827 _, verifiedErr := tx.Get(verifiedKey) 828 if verifiedErr != nil { 829 keys = append(keys, key) 830 } 831 return true 832 }) 833 for _, key := range keys { 834 tx.Delete(key) 835 } 836 return nil 837} 838 839// #1346: remove vhost request queue 840func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error { 841 prefix := "vhostQueue " 842 var keys []string 843 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 844 if !strings.HasPrefix(key, prefix) { 845 return false 846 } 847 keys = append(keys, key) 848 return true 849 }) 850 851 for _, key := range keys { 852 tx.Delete(key) 853 } 854 return nil 855} 856 857// #1274: we used to suspend accounts by deleting their "verified" key, 858// now we save some metadata under a new key 859func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error { 860 now := time.Now().UTC() 861 862 exists := "account.exists " 863 suspended := "account.suspended " 864 verif := "account.verified " 865 verifCode := "account.verificationcode " 866 867 var accounts []string 868 869 tx.AscendGreaterOrEqual("", exists, func(key, value string) bool { 870 if !strings.HasPrefix(key, exists) { 871 return false 872 } 873 account := strings.TrimPrefix(key, exists) 874 _, verifiedErr := tx.Get(verif + account) 875 _, verifCodeErr := tx.Get(verifCode + account) 876 if verifiedErr != nil && verifCodeErr != nil { 877 // verified key not present, but there's no code either, 878 // this is a suspension 879 accounts = append(accounts, account) 880 } 881 return true 882 }) 883 884 type accountSuspensionV18 struct { 885 TimeCreated time.Time 886 Duration time.Duration 887 OperName string 888 Reason string 889 } 890 891 for _, account := range accounts { 892 var sus accountSuspensionV18 893 sus.TimeCreated = now 894 sus.OperName = "*" 895 sus.Reason = "[unknown]" 896 susBytes, err := json.Marshal(sus) 897 if err != nil { 898 return err 899 } 900 tx.Set(suspended+account, string(susBytes), nil) 901 } 902 903 return nil 904} 905 906// #1345: persist the channel-user modes of always-on clients 907func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error { 908 channelToAmodesCache := make(map[string]map[string]modes.Mode) 909 joinedto := "account.joinedto " 910 var accounts []string 911 var channels [][]string 912 tx.AscendGreaterOrEqual("", joinedto, func(key, value string) bool { 913 if !strings.HasPrefix(key, joinedto) { 914 return false 915 } 916 accounts = append(accounts, strings.TrimPrefix(key, joinedto)) 917 var ch []string 918 if value != "" { 919 ch = strings.Split(value, ",") 920 } 921 channels = append(channels, ch) 922 return true 923 }) 924 925 for i := 0; i < len(accounts); i++ { 926 account := accounts[i] 927 channels := channels[i] 928 tx.Delete(joinedto + account) 929 newValue := make(map[string]string, len(channels)) 930 for _, channel := range channels { 931 chcfname, err := CasefoldChannel(channel) 932 if err != nil { 933 continue 934 } 935 // get amodes from the channelToAmodesCache, fill if necessary 936 amodes, ok := channelToAmodesCache[chcfname] 937 if !ok { 938 amodeStr, _ := tx.Get("channel.accounttoumode " + chcfname) 939 if amodeStr != "" { 940 jErr := json.Unmarshal([]byte(amodeStr), &amodes) 941 if jErr != nil { 942 log.Printf("error retrieving amodes for %s: %v\n", channel, jErr) 943 amodes = nil 944 } 945 } 946 // setting/using the nil value here is ok 947 channelToAmodesCache[chcfname] = amodes 948 } 949 if mode, ok := amodes[account]; ok { 950 newValue[channel] = string(mode) 951 } else { 952 newValue[channel] = "" 953 } 954 } 955 newValueBytes, jErr := json.Marshal(newValue) 956 if jErr != nil { 957 log.Printf("couldn't serialize new mode values for v19: %v\n", jErr) 958 continue 959 } 960 tx.Set("account.channeltomodes "+account, string(newValueBytes), nil) 961 } 962 963 return nil 964} 965 966// #1490: start tracking join times for always-on clients 967func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error { 968 type joinData struct { 969 Modes string 970 JoinTime int64 971 } 972 973 var accounts []string 974 var data []string 975 976 now := time.Now().UnixNano() 977 978 prefix := "account.channeltomodes " 979 tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 980 if !strings.HasPrefix(key, prefix) { 981 return false 982 } 983 accounts = append(accounts, strings.TrimPrefix(key, prefix)) 984 data = append(data, value) 985 return true 986 }) 987 988 for i, account := range accounts { 989 var existingMap map[string]string 990 err := json.Unmarshal([]byte(data[i]), &existingMap) 991 if err != nil { 992 return err 993 } 994 newMap := make(map[string]joinData) 995 for channel, modeStr := range existingMap { 996 newMap[channel] = joinData{ 997 Modes: modeStr, 998 JoinTime: now, 999 } 1000 } 1001 serialized, err := json.Marshal(newMap) 1002 if err != nil { 1003 return err 1004 } 1005 tx.Set(prefix+account, string(serialized), nil) 1006 } 1007 1008 return nil 1009} 1010 1011// #734: move the email address into the settings object, 1012// giving people a way to change it 1013func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error { 1014 type accountSettingsv21 struct { 1015 AutoreplayLines *int 1016 NickEnforcement NickEnforcementMethod 1017 AllowBouncer MulticlientAllowedSetting 1018 ReplayJoins ReplayJoinsSetting 1019 AlwaysOn PersistentStatus 1020 AutoreplayMissed bool 1021 DMHistory HistoryStatus 1022 AutoAway PersistentStatus 1023 Email string 1024 } 1025 var accounts []string 1026 var emails []string 1027 callbackPrefix := "account.callback " 1028 tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool { 1029 if !strings.HasPrefix(key, callbackPrefix) { 1030 return false 1031 } 1032 account := strings.TrimPrefix(key, callbackPrefix) 1033 if _, err := tx.Get("account.verified " + account); err != nil { 1034 return true 1035 } 1036 if strings.HasPrefix(value, "mailto:") { 1037 accounts = append(accounts, account) 1038 emails = append(emails, strings.TrimPrefix(value, "mailto:")) 1039 } 1040 return true 1041 }) 1042 for i, account := range accounts { 1043 var settings accountSettingsv21 1044 email := emails[i] 1045 settingsKey := "account.settings " + account 1046 settingsStr, err := tx.Get(settingsKey) 1047 if err == nil && settingsStr != "" { 1048 json.Unmarshal([]byte(settingsStr), &settings) 1049 } 1050 settings.Email = email 1051 settingsBytes, err := json.Marshal(settings) 1052 if err != nil { 1053 log.Printf("couldn't marshal settings for %s: %v\n", account, err) 1054 } else { 1055 tx.Set(settingsKey, string(settingsBytes), nil) 1056 } 1057 tx.Delete(callbackPrefix + account) 1058 } 1059 return nil 1060} 1061 1062// #1676: we used to have ReplayJoinsNever, now it's desupported 1063func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error { 1064 type accountSettingsv22 struct { 1065 AutoreplayLines *int 1066 NickEnforcement NickEnforcementMethod 1067 AllowBouncer MulticlientAllowedSetting 1068 ReplayJoins ReplayJoinsSetting 1069 AlwaysOn PersistentStatus 1070 AutoreplayMissed bool 1071 DMHistory HistoryStatus 1072 AutoAway PersistentStatus 1073 Email string 1074 } 1075 1076 var accounts []string 1077 var serializedSettings []string 1078 settingsPrefix := "account.settings " 1079 tx.AscendGreaterOrEqual("", settingsPrefix, func(key, value string) bool { 1080 if !strings.HasPrefix(key, settingsPrefix) { 1081 return false 1082 } 1083 if value == "" { 1084 return true 1085 } 1086 account := strings.TrimPrefix(key, settingsPrefix) 1087 if _, err := tx.Get("account.verified " + account); err != nil { 1088 return true 1089 } 1090 var settings accountSettingsv22 1091 err := json.Unmarshal([]byte(value), &settings) 1092 if err != nil { 1093 log.Printf("error (v21-22) processing settings for %s: %v\n", account, err) 1094 return true 1095 } 1096 // if necessary, change ReplayJoinsNever (2) to ReplayJoinsCommandsOnly (0) 1097 if settings.ReplayJoins == ReplayJoinsSetting(2) { 1098 settings.ReplayJoins = ReplayJoinsSetting(0) 1099 if b, err := json.Marshal(settings); err == nil { 1100 accounts = append(accounts, account) 1101 serializedSettings = append(serializedSettings, string(b)) 1102 } else { 1103 log.Printf("error (v21-22) processing settings for %s: %v\n", account, err) 1104 } 1105 } 1106 return true 1107 }) 1108 1109 for i, account := range accounts { 1110 tx.Set(settingsPrefix+account, serializedSettings[i], nil) 1111 } 1112 return nil 1113} 1114 1115func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) { 1116 for _, change := range allChanges { 1117 if initialVersion == change.InitialVersion { 1118 return change, true 1119 } 1120 } 1121 return 1122} 1123 1124var allChanges = []SchemaChange{ 1125 { 1126 InitialVersion: 1, 1127 TargetVersion: 2, 1128 Changer: schemaChangeV1toV2, 1129 }, 1130 { 1131 InitialVersion: 2, 1132 TargetVersion: 3, 1133 Changer: schemaChangeV2ToV3, 1134 }, 1135 { 1136 InitialVersion: 3, 1137 TargetVersion: 4, 1138 Changer: schemaChangeV3ToV4, 1139 }, 1140 { 1141 InitialVersion: 4, 1142 TargetVersion: 5, 1143 Changer: schemaChangeV4ToV5, 1144 }, 1145 { 1146 InitialVersion: 5, 1147 TargetVersion: 6, 1148 Changer: schemaChangeV5ToV6, 1149 }, 1150 { 1151 InitialVersion: 6, 1152 TargetVersion: 7, 1153 Changer: schemaChangeV6ToV7, 1154 }, 1155 { 1156 InitialVersion: 7, 1157 TargetVersion: 8, 1158 Changer: schemaChangeV7ToV8, 1159 }, 1160 { 1161 InitialVersion: 8, 1162 TargetVersion: 9, 1163 Changer: schemaChangeV8ToV9, 1164 }, 1165 { 1166 InitialVersion: 9, 1167 TargetVersion: 10, 1168 Changer: schemaChangeV9ToV10, 1169 }, 1170 { 1171 InitialVersion: 10, 1172 TargetVersion: 11, 1173 Changer: schemaChangeV10ToV11, 1174 }, 1175 { 1176 InitialVersion: 11, 1177 TargetVersion: 12, 1178 Changer: schemaChangeV11ToV12, 1179 }, 1180 { 1181 InitialVersion: 12, 1182 TargetVersion: 13, 1183 Changer: schemaChangeV12ToV13, 1184 }, 1185 { 1186 InitialVersion: 13, 1187 TargetVersion: 14, 1188 Changer: schemaChangeV13ToV14, 1189 }, 1190 { 1191 InitialVersion: 14, 1192 TargetVersion: 15, 1193 Changer: schemaChangeV14ToV15, 1194 }, 1195 { 1196 InitialVersion: 15, 1197 TargetVersion: 16, 1198 Changer: schemaChangeV15ToV16, 1199 }, 1200 { 1201 InitialVersion: 16, 1202 TargetVersion: 17, 1203 Changer: schemaChangeV16ToV17, 1204 }, 1205 { 1206 InitialVersion: 17, 1207 TargetVersion: 18, 1208 Changer: schemaChangeV17ToV18, 1209 }, 1210 { 1211 InitialVersion: 18, 1212 TargetVersion: 19, 1213 Changer: schemaChangeV18To19, 1214 }, 1215 { 1216 InitialVersion: 19, 1217 TargetVersion: 20, 1218 Changer: schemaChangeV19To20, 1219 }, 1220 { 1221 InitialVersion: 20, 1222 TargetVersion: 21, 1223 Changer: schemaChangeV20To21, 1224 }, 1225 { 1226 InitialVersion: 21, 1227 TargetVersion: 22, 1228 Changer: schemaChangeV21To22, 1229 }, 1230} 1231