// Copyright (c) 2020 Shivaram Lingamneni // released under the MIT license package irc import ( "encoding/json" "fmt" "log" "os" "strconv" "github.com/tidwall/buntdb" "github.com/ergochat/ergo/irc/utils" ) const ( // produce a hardcoded version of the database schema // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal // (to ensure that no matter what code changes happen elsewhere, we're still producing a // db of the hardcoded version) importDBSchemaVersion = 19 ) type userImport struct { Name string Hash string Email string RegisteredAt int64 `json:"registeredAt"` Vhost string AdditionalNicks []string `json:"additionalNicks"` Certfps []string } type channelImport struct { Name string Founder string RegisteredAt int64 `json:"registeredAt"` Topic string TopicSetBy string `json:"topicSetBy"` TopicSetAt int64 `json:"topicSetAt"` Amode map[string]string Modes string Key string Limit int Forward string } type databaseImport struct { Version int Source string Users map[string]userImport Channels map[string]channelImport } func serializeAmodes(raw map[string]string, validCfUsernames utils.StringSet) (result []byte, err error) { processed := make(map[string]int, len(raw)) for accountName, mode := range raw { if len(mode) != 1 { return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName) } cfname, err := CasefoldName(accountName) if err != nil || !validCfUsernames.Has(cfname) { log.Printf("skipping invalid amode recipient %s\n", accountName) } else { processed[cfname] = int(mode[0]) } } result, err = json.Marshal(processed) return } func doImportDBGeneric(config *Config, dbImport databaseImport, credsType CredentialsVersion, tx *buntdb.Tx) (err error) { requiredVersion := 1 if dbImport.Version != requiredVersion { return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion) } tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) cfUsernames := make(utils.StringSet) skeletonToUsername := make(map[string]string) warnSkeletons := false for username, userInfo := range dbImport.Users { cfUsername, err := CasefoldName(username) skeleton, skErr := Skeleton(username) if err != nil || skErr != nil { log.Printf("invalid username %s: %v\n", username, err) continue } if existingSkelUser, ok := skeletonToUsername[skeleton]; ok { log.Printf("Users %s and %s have confusable nicknames; this may render one or both accounts unusable\n", username, existingSkelUser) warnSkeletons = true } else { skeletonToUsername[skeleton] = username } var certfps []string for _, certfp := range userInfo.Certfps { normalizedCertfp, err := utils.NormalizeCertfp(certfp) if err == nil { certfps = append(certfps, normalizedCertfp) } else { log.Printf("invalid certfp %s for %s\n", username, certfp) } } credentials := AccountCredentials{ Version: credsType, PassphraseHash: []byte(userInfo.Hash), Certfps: certfps, } marshaledCredentials, err := json.Marshal(&credentials) if err != nil { log.Printf("invalid credentials for %s: %v\n", username, err) continue } tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil) tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil) tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil) tx.Set(fmt.Sprintf(keyAccountCallback, cfUsername), "mailto:"+userInfo.Email, nil) tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil) tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil) if userInfo.Vhost != "" { vhinfo := VHostInfo{ Enabled: true, ApprovedVHost: userInfo.Vhost, } vhBytes, err := json.Marshal(vhinfo) if err == nil { tx.Set(fmt.Sprintf(keyAccountVHost, cfUsername), string(vhBytes), nil) } else { log.Printf("couldn't serialize vhost for %s: %v\n", username, err) } } if len(userInfo.AdditionalNicks) != 0 { tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil) } for _, certfp := range certfps { tx.Set(fmt.Sprintf(keyCertToAccount, certfp), cfUsername, nil) } cfUsernames.Add(cfUsername) } for chname, chInfo := range dbImport.Channels { cfchname, err := CasefoldChannel(chname) if err != nil { log.Printf("invalid channel name %s: %v", chname, err) continue } cffounder, err := CasefoldName(chInfo.Founder) if err != nil { log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err) continue } tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil) tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil) accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder) founderChannels, fcErr := tx.Get(accountChannelsKey) if fcErr != nil || founderChannels == "" { founderChannels = cfchname } else { founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname) } tx.Set(accountChannelsKey, founderChannels, nil) if chInfo.Topic != "" { tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil) tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil) tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil) } if len(chInfo.Amode) != 0 { m, err := serializeAmodes(chInfo.Amode, cfUsernames) if err == nil { tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil) } else { log.Printf("couldn't serialize amodes for %s: %v", chname, err) } } tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil) if chInfo.Key != "" { tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil) } if chInfo.Limit > 0 { tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil) } if chInfo.Forward != "" { if _, err := CasefoldChannel(chInfo.Forward); err == nil { tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil) } } } if warnSkeletons { log.Printf("NOTE: you may be able to avoid confusability issues by changing the server casemapping setting to `ascii`\n") log.Printf("However, this will prevent the use of non-ASCII Unicode characters in nicknames\n") } return nil } func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) { switch dbImport.Source { case "atheme": return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx) case "anope": return doImportDBGeneric(config, dbImport, CredentialsAnope, tx) default: return fmt.Errorf("unsupported import source: %s", dbImport.Source) } } func ImportDB(config *Config, infile string) (err error) { data, err := os.ReadFile(infile) if err != nil { return } var dbImport databaseImport err = json.Unmarshal(data, &dbImport) if err != nil { return err } err = checkDBReadyForInit(config.Datastore.Path) if err != nil { return err } db, err := buntdb.Open(config.Datastore.Path) if err != nil { return err } performImport := func(tx *buntdb.Tx) (err error) { return doImportDB(config, dbImport, tx) } return db.Update(performImport) }