123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- // Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
- // 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)
- }
|