mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-23 19:22:41 +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.
247 lines
7.6 KiB
Go
247 lines
7.6 KiB
Go
// 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)
|
|
settings := AccountSettings{Email: userInfo.Email}
|
|
settingsBytes, _ := json.Marshal(settings)
|
|
tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), 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)
|
|
}
|