retrofitting

This commit is contained in:
kayos@tcp.direct 2021-08-27 00:04:24 -07:00
commit e962e1f2aa
46 changed files with 628 additions and 201 deletions

@ -19,7 +19,7 @@ jobs:
- name: "setup go"
uses: "actions/setup-go@v2"
with:
go-version: "1.16.4"
go-version: "1.17"
- name: "install python3-pytest"
run: "sudo apt install -y python3-pytest"
- name: "make install"

@ -1,5 +1,5 @@
## build ergo binary
FROM golang:1.16-alpine3.13 AS build-env
FROM golang:1.17-alpine AS build-env
RUN apk add -U --force-refresh --no-cache --purge --clean-protected -l -u make

@ -414,6 +414,13 @@ accounts:
blacklist-regexes:
# - ".*@mailinator.com"
timeout: 60s
# email-based password reset:
password-reset:
enabled: false
# time before we allow resending the email
cooldown: 1h
# time for which a password reset code is valid
timeout: 1d
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)

@ -183,7 +183,7 @@ The only major distribution that currently packages Ergo is Arch Linux; the afor
1. Create a dedicated, unprivileged role user who will own the ergo process and all its associated files: `adduser --system --group ergo`. This user now has a home directory at `/home/ergo`. To prevent other users from viewing Ergo's configuration file, database, and certificates, restrict the permissions on the home directory: `chmod 0700 /home/ergo`.
1. Copy the executable binary `ergo`, the config file `ircd.yaml`, the database `ircd.db`, and the self-signed TLS certificate (`fullchain.pem` and `privkey.pem`) to `/home/ergo`. (If you don't have an `ircd.db`, it will be auto-created as `/home/ergo/ircd.db` on first launch.) Ensure that they are all owned by the new ergo role user: `sudo chown ergo:ergo /home/ergo/*`. Ensure that the configuration file logs to stderr.
1. Install our example [ergo.service](https://github.com/ergochat/ergo/blob/master/distrib/systemd/ergo.service) file to `/etc/systemd/system/ergo.service`.
1. Install our example [ergo.service](https://github.com/ergochat/ergo/blob/stable/distrib/systemd/ergo.service) file to `/etc/systemd/system/ergo.service`.
1. Enable and start the new service with the following commands:
1. `systemctl daemon-reload`
1. `systemctl enable ergo.service`

@ -177,6 +177,12 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/435",
standard="draft IRCv3",
),
CapDef(
identifier="ExtendedMonitor",
name="draft/extended-monitor",
url="https://github.com/ircv3/ircv3-specifications/pull/466",
standard="draft IRCv3",
),
]
def validate_defs():

15
go.mod

@ -1,6 +1,6 @@
module github.com/ergochat/ergo
go 1.16
go 1.17
require (
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
@ -25,6 +25,19 @@ require (
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/tidwall/btree v0.6.0 // indirect
github.com/tidwall/gjson v1.8.0 // indirect
github.com/tidwall/grect v0.1.2 // indirect
github.com/tidwall/match v1.0.3 // indirect
github.com/tidwall/pretty v1.1.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
)
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
replace github.com/xdg-go/scram => github.com/ergochat/scram v1.0.2-ergo1

8
go.sum

@ -39,20 +39,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/btree v0.4.2 h1:aLwwJlG+InuFzdAPuBf9YCAR1LvSQ9zhC5aorFPlIPs=
github.com/tidwall/btree v0.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v0.6.0 h1:JLYAFGV+1gjyFi3iQbO/fupBin+Ooh7dxqVV0twJ1Bo=
github.com/tidwall/btree v0.6.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
github.com/tidwall/buntdb v1.2.3 h1:AoGVe4yrhKmnEPHrPrW5EUOATHOCIk4VtFvd8xn/ZtU=
github.com/tidwall/buntdb v1.2.3/go.mod h1:+i/gBwYOHWG19wLgwMXFLkl00twh9+VWkkaOhuNQ4PA=
github.com/tidwall/buntdb v1.2.6 h1:eS0QSmzHfCKjxxYGh8eH6wnK5VLsJ7UjyyIr29JmnEg=
github.com/tidwall/buntdb v1.2.6/go.mod h1:zpXqlA5D2772I4cTqV3ifr2AZihDgi8FV7xAQu6edfc=
github.com/tidwall/gjson v1.7.4 h1:19cchw8FOxkG5mdLRkGf9jqIqEyqdZhPqW60XfyFxk8=
github.com/tidwall/gjson v1.7.4/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/grect v0.1.1 h1:+kMEkxhoqB7rniVXzMEIA66XwU07STgINqxh+qVIndY=
github.com/tidwall/grect v0.1.1/go.mod h1:CzvbGiFbWUwiJ1JohXLb28McpyBsI00TK9Y6pDWLGRQ=
github.com/tidwall/grect v0.1.2 h1:wKVeQVZhjaFCKTTlpkDe3Ex4ko3cMGW3MRKawRe8uQ4=
github.com/tidwall/grect v0.1.2/go.mod h1:v+n4ewstPGduVJebcp5Eh2WXBJBumNzyhK8GZt4gHNw=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=

@ -4,7 +4,6 @@
package irc
import (
"bytes"
"crypto/rand"
"crypto/x509"
"encoding/json"
@ -32,7 +31,6 @@ const (
keyAccountExists = "account.exists %s"
keyAccountVerified = "account.verified %s"
keyAccountUnregistered = "account.unregistered %s"
keyAccountCallback = "account.callback %s"
keyAccountVerificationCode = "account.verificationcode %s"
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
keyAccountRegTime = "account.registered.time %s"
@ -46,6 +44,8 @@ const (
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
keyAccountRealname = "account.realname %s" // client realname stored as string
keyAccountSuspended = "account.suspended %s" // client realname stored as string
keyAccountPwReset = "account.pwreset %s"
keyAccountEmailChange = "account.emailchange %s"
// for an always-on client, a map of channel names they're in to their current modes
// (not to be confused with their amodes, which a non-always-on client can have):
keyAccountChannelToModes = "account.channeltomodes %s"
@ -391,10 +391,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
var creds AccountCredentials
@ -409,8 +409,16 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
return err
}
var settingsStr string
if callbackNamespace == "mailto" {
settings := AccountSettings{Email: callbackValue}
j, err := json.Marshal(settings)
if err == nil {
settingsStr = string(j)
}
}
registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
var setOptions *buntdb.SetOptions
ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
@ -449,7 +457,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
tx.Set(accountNameKey, account, setOptions)
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
tx.Set(credentialsKey, credStr, setOptions)
tx.Set(callbackKey, callbackSpec, setOptions)
tx.Set(settingsKey, settingsStr, setOptions)
if certfp != "" {
tx.Set(certFPKey, casefoldedAccount, setOptions)
}
@ -782,15 +790,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, account string,
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
}
var message bytes.Buffer
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
if config.DKIM.Domain != "" {
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
}
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
message.WriteString("\r\n") // blank line: end headers, begin message body
message := email.ComposeMail(config, callbackValue, subject)
fmt.Fprintf(&message, client.t("Account: %s"), account)
message.WriteString("\r\n")
fmt.Fprintf(&message, client.t("Verification code: %s"), code)
@ -823,8 +823,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
var raw rawClientAccount
@ -892,8 +892,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
tx.Set(accountKey, "1", nil)
tx.Set(accountNameKey, raw.Name, nil)
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
tx.Set(callbackKey, raw.Callback, nil)
tx.Set(credentialsKey, raw.Credentials, nil)
tx.Set(settingsKey, raw.Settings, nil)
var creds AccountCredentials
// XXX we shouldn't do (de)serialization inside the txn,
@ -955,6 +955,214 @@ func (am *AccountManager) SARegister(account, passphrase string) (err error) {
return
}
type EmailChangeRecord struct {
TimeCreated time.Time
Code string
Email string
}
func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err error) {
casefoldedAccount := client.Account()
if casefoldedAccount == "" {
return errAccountNotLoggedIn
}
if am.touchRegisterThrottle() {
am.server.logger.Warning("accounts", "global registration throttle exceeded by client changing email", client.Nick())
return errLimitExceeded
}
config := am.server.Config()
if !config.Accounts.Registration.EmailVerification.Enabled {
return errFeatureDisabled // redundant check, just in case
}
record := EmailChangeRecord{
TimeCreated: time.Now().UTC(),
Code: utils.GenerateSecretToken(),
Email: emailAddr,
}
recordKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
recordBytes, _ := json.Marshal(record)
recordVal := string(recordBytes)
am.server.store.Update(func(tx *buntdb.Tx) error {
tx.Set(recordKey, recordVal, nil)
return nil
})
if err != nil {
return err
}
message := email.ComposeMail(config.Accounts.Registration.EmailVerification,
emailAddr,
fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.name))
message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.name))
message.WriteString("\r\n")
fmt.Fprintf(&message, "/MSG NickServ VERIFYEMAIL %s\r\n", record.Code)
err = email.SendMail(config.Accounts.Registration.EmailVerification, emailAddr, message.Bytes())
if err == nil {
am.server.logger.Info("services",
fmt.Sprintf("email change verification sent for account %s", casefoldedAccount))
return
} else {
am.server.logger.Error("internal", "Failed to dispatch e-mail change verification to", emailAddr, err.Error())
return &registrationCallbackError{err}
}
}
func (am *AccountManager) NsVerifyEmail(client *Client, code string) (err error) {
casefoldedAccount := client.Account()
if casefoldedAccount == "" {
return errAccountNotLoggedIn
}
var record EmailChangeRecord
success := false
key := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
ttl := time.Duration(am.server.Config().Accounts.Registration.VerifyTimeout)
am.server.store.Update(func(tx *buntdb.Tx) error {
rawStr, err := tx.Get(key)
if err == nil && rawStr != "" {
err := json.Unmarshal([]byte(rawStr), &record)
if err == nil {
if (ttl == 0 || time.Since(record.TimeCreated) < ttl) && utils.SecretTokensMatch(record.Code, code) {
success = true
tx.Delete(key)
}
}
}
return nil
})
if !success {
return errAccountVerificationInvalidCode
}
munger := func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.Email = record.Email
return
}
_, err = am.ModifyAccountSettings(casefoldedAccount, munger)
return
}
func (am *AccountManager) NsSendpass(client *Client, accountName string) (err error) {
config := am.server.Config()
if !(config.Accounts.Registration.EmailVerification.Enabled && config.Accounts.Registration.EmailVerification.PasswordReset.Enabled) {
return errFeatureDisabled
}
account, err := am.LoadAccount(accountName)
if err != nil {
return err
}
if !account.Verified {
return errAccountUnverified
}
if account.Suspended != nil {
return errAccountSuspended
}
if account.Settings.Email == "" {
return errValidEmailRequired
}
record := PasswordResetRecord{
TimeCreated: time.Now().UTC(),
Code: utils.GenerateSecretToken(),
}
recordKey := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
recordBytes, _ := json.Marshal(record)
recordVal := string(recordBytes)
am.server.store.Update(func(tx *buntdb.Tx) error {
recStr, recErr := tx.Get(recordKey)
if recErr == nil && recStr != "" {
var existing PasswordResetRecord
jErr := json.Unmarshal([]byte(recStr), &existing)
cooldown := time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Cooldown)
if jErr == nil && time.Since(existing.TimeCreated) < cooldown {
err = errLimitExceeded
return nil
}
}
tx.Set(recordKey, recordVal, &buntdb.SetOptions{
Expires: true,
TTL: time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Timeout),
})
return nil
})
if err != nil {
return
}
subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.name)
message := email.ComposeMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, subject)
fmt.Fprintf(&message, client.t("We received a request to reset your password on %s for account: %s"), am.server.name, account.Name)
message.WriteString("\r\n")
fmt.Fprintf(&message, client.t("If you did not initiate this request, you can safely ignore this message."))
message.WriteString("\r\n")
message.WriteString("\r\n")
message.WriteString(client.t("Otherwise, to reset your password, issue the following command (replace `new_password` with your desired password):"))
message.WriteString("\r\n")
fmt.Fprintf(&message, "/MSG NickServ RESETPASS %s %s new_password\r\n", account.Name, record.Code)
err = email.SendMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, message.Bytes())
if err == nil {
am.server.logger.Info("services",
fmt.Sprintf("client %s sent a password reset email for account %s", client.Nick(), account.Name))
} else {
am.server.logger.Error("internal", "Failed to dispatch e-mail to", account.Settings.Email, err.Error())
}
return
}
func (am *AccountManager) NsResetpass(client *Client, accountName, code, password string) (err error) {
if validatePassphrase(password) != nil {
return errAccountBadPassphrase
}
account, err := am.LoadAccount(accountName)
if err != nil {
return
}
if !account.Verified {
return errAccountUnverified
}
if account.Suspended != nil {
return errAccountSuspended
}
success := false
key := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
am.server.store.Update(func(tx *buntdb.Tx) error {
rawStr, err := tx.Get(key)
if err == nil && rawStr != "" {
var record PasswordResetRecord
err := json.Unmarshal([]byte(rawStr), &record)
if err == nil && utils.SecretTokensMatch(record.Code, code) {
success = true
tx.Delete(key)
}
}
return nil
})
if success {
return am.setPassword(accountName, password, true)
} else {
return errAccountInvalidCredentials
}
}
type PasswordResetRecord struct {
TimeCreated time.Time
Code string
}
func marshalReservedNicks(nicks []string) string {
return strings.Join(nicks, ",")
}
@ -1294,9 +1502,6 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName str
return
}
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
if strings.HasPrefix(raw.Callback, "mailto:") {
result.Email = strings.TrimPrefix(raw.Callback, "mailto:")
}
result.Verified = raw.Verified
if raw.VHost != "" {
e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
@ -1329,7 +1534,6 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
@ -1344,7 +1548,6 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
result.Name, _ = tx.Get(accountNameKey)
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
result.Credentials, _ = tx.Get(credentialsKey)
result.Callback, _ = tx.Get(callbackKey)
result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey)
result.Settings, _ = tx.Get(settingsKey)
@ -1524,7 +1727,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
@ -1537,6 +1739,8 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
var clients []*Client
defer func() {
@ -1582,7 +1786,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
tx.Delete(accountNameKey)
tx.Delete(verifiedKey)
tx.Delete(registeredTimeKey)
tx.Delete(callbackKey)
tx.Delete(verificationCodeKey)
tx.Delete(settingsKey)
rawNicks, _ = tx.Get(nicksKey)
@ -1597,6 +1800,8 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
tx.Delete(modesKey)
tx.Delete(realnameKey)
tx.Delete(suspendedKey)
tx.Delete(pwResetKey)
tx.Delete(emailChangeKey)
return nil
})
@ -1940,17 +2145,19 @@ const (
CredentialsAnope = -2
)
type SCRAMCreds struct {
Salt []byte
Iters int
StoredKey []byte
ServerKey []byte
}
// AccountCredentials stores the various methods for verifying accounts.
type AccountCredentials struct {
Version CredentialsVersion
PassphraseHash []byte
Certfps []string
SCRAMCreds struct {
Salt []byte
Iters int
StoredKey []byte
ServerKey []byte
}
SCRAMCreds
}
func (ac *AccountCredentials) Empty() bool {
@ -1970,6 +2177,7 @@ func (ac *AccountCredentials) Serialize() (result string, err error) {
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
if passphrase == "" {
ac.PassphraseHash = nil
ac.SCRAMCreds = SCRAMCreds{}
return nil
}
@ -1994,10 +2202,12 @@ func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint)
// xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
minIters := 4096
scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
ac.SCRAMCreds.Salt = salt
ac.SCRAMCreds.Iters = minIters
ac.SCRAMCreds.StoredKey = scramCreds.StoredKey
ac.SCRAMCreds.ServerKey = scramCreds.ServerKey
ac.SCRAMCreds = SCRAMCreds{
Salt: salt,
Iters: minIters,
StoredKey: scramCreds.StoredKey,
ServerKey: scramCreds.ServerKey,
}
return nil
}
@ -2112,6 +2322,7 @@ type AccountSettings struct {
AutoreplayMissed bool
DMHistory HistoryStatus
AutoAway PersistentStatus
Email string
}
// ClientAccount represents a user account.
@ -2120,7 +2331,6 @@ type ClientAccount struct {
Name string
NameCasefolded string
RegisteredAt time.Time
Email string
Credentials AccountCredentials
Verified bool
Suspended *AccountSuspension
@ -2134,7 +2344,6 @@ type rawClientAccount struct {
Name string
RegisteredAt string
Credentials string
Callback string
Verified bool
AdditionalNicks string
VHost string

@ -7,7 +7,7 @@ package caps
const (
// number of recognized capabilities:
numCapabs = 27
numCapabs = 28
// length of the uint64 array that represents the bitset:
bitsetLen = 1
)
@ -53,6 +53,10 @@ const (
// https://github.com/ircv3/ircv3-specifications/pull/362
EventPlayback Capability = iota
// ExtendedMonitor is the draft IRCv3 capability named "draft/extended-monitor":
// https://github.com/ircv3/ircv3-specifications/pull/466
ExtendedMonitor Capability = iota
// Languages is the proposed IRCv3 capability named "draft/languages":
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
Languages Capability = iota
@ -135,6 +139,7 @@ var (
"draft/channel-rename",
"draft/chathistory",
"draft/event-playback",
"draft/extended-monitor",
"draft/languages",
"draft/multiline",
"draft/relaymsg",

@ -613,11 +613,7 @@ func (client *Client) getIPNoMutex() net.IP {
// IPString returns the IP address of this client as a string.
func (client *Client) IPString() string {
ip := client.IP().String()
if 0 < len(ip) && ip[0] == ':' {
ip = "0" + ip
}
return ip
return utils.IPStringToHostname(client.IP().String())
}
// t returns the translated version of the given string, based on the languages configured by the client.
@ -1029,6 +1025,13 @@ func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]em
return
}
// Friends refers to clients that share a channel or extended-monitor this client.
func (client *Client) FriendsMonitors(capabs ...caps.Capability) (result map[*Session]empty) {
result = client.Friends(capabs...)
client.server.monitorManager.AddMonitors(result, client.nickCasefolded, capabs...)
return
}
// helper for Friends
func addFriendsToSet(set map[*Session]empty, client *Client, capabs ...caps.Capability) {
client.stateMutex.RLock()
@ -1053,7 +1056,7 @@ func (client *Client) SetOper(oper *Oper) {
func (client *Client) sendChghost(oldNickMask string, vhost string) {
details := client.Details()
isBot := client.HasMode(modes.Bot)
for fClient := range client.Friends(caps.ChgHost) {
for fClient := range client.FriendsMonitors(caps.ChgHost) {
fClient.sendFromClientInternal(false, time.Time{}, "", oldNickMask, details.accountName, isBot, nil, "CHGHOST", details.username, vhost)
}
}

@ -303,6 +303,7 @@ func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err e
type AccountConfig struct {
Registration AccountRegistrationConfig
AuthenticationEnabled bool `yaml:"authentication-enabled"`
AdvertiseSCRAM bool `yaml:"advertise-scram"` // undocumented, see #1782
RequireSasl struct {
Enabled bool
Exempted []string
@ -1382,7 +1383,12 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
}
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL,SCRAM-SHA-256"
saslCapValue := "PLAIN,EXTERNAL,SCRAM-SHA-256"
// TODO(#1782) clean this up:
if !config.Accounts.AdvertiseSCRAM {
saslCapValue = "PLAIN,EXTERNAL"
}
config.Server.capValues[caps.SASL] = saslCapValue
if !config.Accounts.AuthenticationEnabled {
config.Server.supportedCaps.Disable(caps.SASL)
}
@ -1591,7 +1597,7 @@ func (config *Config) generateISupport() (err error) {
isupport.Add("RPUSER", "E")
}
isupport.Add("STATUSMSG", "~&@%+")
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:%d", maxTargetsString, maxTargetsString, maxTargetsString, config.Limits.MonitorEntries))
isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
if config.Server.Casemapping == CasemappingPRECIS {
isupport.Add("UTF8MAPPING", precisUTF8MappingToken)

@ -24,7 +24,7 @@ const (
// 'version' of the database schema
keySchemaVersion = "db.version"
// latest schema of the db
latestDbSchema = 20
latestDbSchema = 21
keyCloakSecret = "crypto.cloak_secret"
)
@ -1008,6 +1008,57 @@ func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
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 {
@ -1113,4 +1164,9 @@ var allChanges = []SchemaChange{
TargetVersion: 20,
Changer: schemaChangeV19To20,
},
{
InitialVersion: 20,
TargetVersion: 21,
Changer: schemaChangeV20To21,
},
}

@ -4,6 +4,7 @@
package email
import (
"bytes"
"errors"
"fmt"
"net"
@ -11,7 +12,9 @@ import (
"strings"
"time"
"github.com/ergochat/ergo/irc/custime"
"github.com/ergochat/ergo/irc/smtp"
"github.com/ergochat/ergo/irc/utils"
)
var (
@ -42,6 +45,11 @@ type MailtoConfig struct {
BlacklistRegexes []string `yaml:"blacklist-regexes"`
blacklistRegexes []*regexp.Regexp
Timeout time.Duration
PasswordReset struct {
Enabled bool
Cooldown custime.Duration
Timeout custime.Duration
} `yaml:"password-reset"`
}
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
@ -95,6 +103,19 @@ func lookupMX(domain string) (server string) {
return
}
func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
fmt.Fprintf(&message, "To: %s\r\n", recipient)
dkimDomain := config.DKIM.Domain
if dkimDomain != "" {
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
}
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
message.WriteString("\r\n") // blank line: end headers, begin message body
return message
}
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
for _, reg := range config.blacklistRegexes {
if reg.MatchString(recipient) {

@ -145,6 +145,17 @@ func (client *Client) removeSession(session *Session) (success bool, length int)
return
}
// #1650: show an arbitrarily chosen session IP and hostname in RPL_WHOISACTUALLY
func (client *Client) getWhoisActually() (ip net.IP, hostname string) {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
for _, session := range client.sessions {
return session.IP(), session.rawHostname
}
return utils.IPv4LoopbackAddress, client.server.name
}
func (client *Client) Nick() string {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()

@ -109,7 +109,7 @@ func sendSuccessfulAccountAuth(service *ircService, client *Client, rb *Response
if client.Registered() {
// dispatch account-notify
for friend := range client.Friends(caps.AccountNotify) {
for friend := range client.FriendsMonitors(caps.AccountNotify) {
if friend != rb.session {
friend.Send(nil, details.nickMask, "ACCOUNT", details.accountName)
}
@ -421,7 +421,7 @@ func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) {
// dispatch away-notify
details := client.Details()
isBot := client.HasMode(modes.Bot)
for session := range client.Friends(caps.AwayNotify) {
for session := range client.FriendsMonitors(caps.AwayNotify) {
if isAway {
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage)
} else {
@ -1315,6 +1315,9 @@ func kickHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
if len(msg.Params) > 2 {
comment = msg.Params[2]
}
if comment == "" {
comment = client.Nick()
}
for _, kick := range kicks {
channel := server.channels.Get(kick.channel)
if channel == nil {
@ -1327,10 +1330,6 @@ func kickHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
rb.Add(nil, server.name, ERR_NOSUCHNICK, client.nick, utils.SafeErrorParam(kick.nick), client.t("No such nick"))
continue
}
if comment == "" {
comment = kick.nick
}
channel.Kick(client, target, comment, rb, hasPrivs)
}
return false
@ -2869,7 +2868,7 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
// alert friends
now := time.Now().UTC()
friends := client.Friends(caps.SetName)
friends := client.FriendsMonitors(caps.SetName)
delete(friends, rb.session)
isBot := client.HasMode(modes.Bot)
for session := range friends {
@ -2974,7 +2973,7 @@ func userHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
username, realname := msg.Params[0], msg.Params[3]
if len(realname) == 0 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), client.t("Not enough parameters"))
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "USER", client.t("Not enough parameters"))
return false
}

@ -121,7 +121,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
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)
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 != "" {

@ -6,6 +6,8 @@ package irc
import (
"sync"
"github.com/ergochat/ergo/irc/caps"
"github.com/ergochat/irc-go/ircmsg"
)
@ -23,6 +25,17 @@ func (mm *MonitorManager) Initialize() {
mm.watchedby = make(map[string]map[*Session]empty)
}
// AddMonitors adds clients using extended-monitor monitoring `client`'s nick to the passed user set.
func (manager *MonitorManager) AddMonitors(users map[*Session]empty, cfnick string, capabs ...caps.Capability) {
manager.RLock()
defer manager.RUnlock()
for session := range manager.watchedby[cfnick] {
if session.capabilities.Has(caps.ExtendedMonitor) && session.capabilities.HasAll(capabs...) {
users[session] = empty{}
}
}
}
// AlertAbout alerts everyone monitoring `client`'s nick that `client` is now {on,off}line.
func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) {
var watchers []*Session

@ -36,6 +36,11 @@ func servCmdRequiresBouncerEnabled(config *Config) bool {
return config.Accounts.Multiclient.Enabled
}
func servCmdRequiresEmailReset(config *Config) bool {
return config.Accounts.Registration.EmailVerification.Enabled &&
config.Accounts.Registration.EmailVerification.PasswordReset.Enabled
}
const nickservHelp = `NickServ lets you register, log in to, and manage an account.`
var (
@ -302,6 +307,12 @@ how the history of your direct messages is stored. Your options are:
'auto-away' is only effective for always-on clients. If enabled, you will
automatically be marked away when all your sessions are disconnected, and
automatically return from away when you connect again.`,
`$bEMAIL$b
'email' controls the e-mail address associated with your account (if the
server operator allows it, this address can be used for password resets).
As an additional security measure, if you have a password set, you must
provide it as an additional argument to $bSET$b, for example,
SET EMAIL test@example.com hunter2`,
},
authRequired: true,
enabled: servCmdRequiresAuthEnabled,
@ -318,6 +329,27 @@ information on the settings and their possible values, see HELP SET.`,
minParams: 3,
capabs: []string{"accreg"},
},
"sendpass": {
handler: nsSendpassHandler,
help: `Syntax: $bSENDPASS <account>$b
SENDPASS sends a password reset email to the email address associated with
the target account. The reset code in the email can then be used with the
$bRESETPASS$b command.`,
helpShort: `$bSENDPASS$b initiates an email-based password reset`,
enabled: servCmdRequiresEmailReset,
minParams: 1,
},
"resetpass": {
handler: nsResetpassHandler,
help: `Syntax: $bRESETPASS <account> <code> <password>$b
RESETPASS resets an account password, using a reset code that was emailed as
the result of a previous $bSENDPASS$b command.`,
helpShort: `$bRESETPASS$b completes an email-based password reset`,
enabled: servCmdRequiresEmailReset,
minParams: 3,
},
"cert": {
handler: nsCertHandler,
help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
@ -357,6 +389,12 @@ Currently, you can only change the canonical casefolding of an account
minParams: 2,
capabs: []string{"accreg"},
},
"verifyemail": {
handler: nsVerifyEmailHandler,
authRequired: true,
minParams: 1,
hidden: true,
},
}
)
@ -459,7 +497,12 @@ func displaySetting(service *ircService, settingName string, settings AccountSet
effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
service.Notice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
case "email":
if settings.Email != "" {
service.Notice(rb, fmt.Sprintf(client.t("Your stored e-mail address is: %s"), settings.Email))
} else {
service.Notice(rb, client.t("You have no stored e-mail address"))
}
default:
service.Notice(rb, client.t("No such setting"))
}
@ -475,18 +518,27 @@ func userPersistentStatusToString(status PersistentStatus) string {
}
func nsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var privileged bool
var account string
if command == "saset" {
privileged = true
account = params[0]
params = params[1:]
} else {
account = client.Account()
}
key := strings.ToLower(params[0])
// unprivileged NS SET EMAIL is different because it requires a confirmation
if !privileged && key == "email" {
nsSetEmailHandler(service, client, params, rb)
return
}
var munger settingsMunger
var finalSettings AccountSettings
var err error
switch strings.ToLower(params[0]) {
switch key {
case "pass", "password":
service.Notice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
return
@ -603,6 +655,13 @@ func nsSetHandler(service *ircService, server *Server, client *Client, command s
return
}
}
case "email":
newValue := params[1]
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.Email = newValue
return
}
default:
err = errInvalidParams
}
@ -614,7 +673,7 @@ func nsSetHandler(service *ircService, server *Server, client *Client, command s
switch err {
case nil:
service.Notice(rb, client.t("Successfully changed your account settings"))
displaySetting(service, params[0], finalSettings, client, rb)
displaySetting(service, key, finalSettings, client, rb)
case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
service.Notice(rb, client.t(err.Error()))
case errNickAccountMismatch:
@ -625,6 +684,55 @@ func nsSetHandler(service *ircService, server *Server, client *Client, command s
}
}
// handle unprivileged NS SET EMAIL, which sends a confirmation code
func nsSetEmailHandler(service *ircService, client *Client, params []string, rb *ResponseBuffer) {
config := client.server.Config()
if !config.Accounts.Registration.EmailVerification.Enabled {
rb.Notice(client.t("E-mail verification is disabled"))
return
}
if !nsLoginThrottleCheck(service, client, rb) {
return
}
var password string
if len(params) > 2 {
password = params[2]
}
account := client.Account()
errorMessage := nsConfirmPassword(client.server, account, password)
if errorMessage != "" {
service.Notice(rb, client.t(errorMessage))
return
}
err := client.server.accounts.NsSetEmail(client, params[1])
switch err {
case nil:
service.Notice(rb, client.t("Check your e-mail for instructions on how to confirm your change of address"))
case errLimitExceeded:
service.Notice(rb, client.t("Try again later"))
default:
// if appropriate, show the client the error from the attempted email sending
if rErr := registrationCallbackErrorText(config, client, err); rErr != "" {
service.Notice(rb, rErr)
} else {
service.Notice(rb, client.t("An error occurred"))
}
}
}
func nsVerifyEmailHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
err := server.accounts.NsVerifyEmail(client, params[0])
switch err {
case nil:
service.Notice(rb, client.t("Successfully changed your account settings"))
displaySetting(service, "email", client.AccountSettings(), client, rb)
case errAccountVerificationInvalidCode:
service.Notice(rb, client.t(err.Error()))
default:
service.Notice(rb, client.t("An error occurred"))
}
}
func nsDropHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
sadrop := command == "sadrop"
var nick string
@ -815,8 +923,8 @@ func nsInfoHandler(service *ircService, server *Server, client *Client, command
service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
if account.Name == client.AccountName() || client.HasRoleCapabs("accreg") {
if account.Email != "" {
service.Notice(rb, fmt.Sprintf(client.t("Email address: %s"), account.Email))
if account.Settings.Email != "" {
service.Notice(rb, fmt.Sprintf(client.t("Email address: %s"), account.Settings.Email))
}
}
@ -1018,6 +1126,19 @@ func nsVerifyHandler(service *ircService, server *Server, client *Client, comman
}
}
func nsConfirmPassword(server *Server, account, passphrase string) (errorMessage string) {
accountData, err := server.accounts.LoadAccount(account)
if err != nil {
errorMessage = `You're not logged into an account`
} else {
hash := accountData.Credentials.PassphraseHash
if hash != nil && passwd.CompareHashAndPassword(hash, []byte(passphrase)) != nil {
errorMessage = `Password incorrect`
}
}
return
}
func nsPasswdHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var target string
var newPassword string
@ -1041,28 +1162,19 @@ func nsPasswdHandler(service *ircService, server *Server, client *Client, comman
}
case 3:
target = client.Account()
newPassword = params[1]
if newPassword == "*" {
newPassword = ""
}
if target == "" {
errorMessage = `You're not logged into an account`
} else if params[1] != params[2] {
} else if newPassword != params[2] {
errorMessage = `Passwords do not match`
} else {
if !nsLoginThrottleCheck(service, client, rb) {
return
}
accountData, err := server.accounts.LoadAccount(target)
if err != nil {
errorMessage = `You're not logged into an account`
} else {
hash := accountData.Credentials.PassphraseHash
if hash != nil && passwd.CompareHashAndPassword(hash, []byte(params[0])) != nil {
errorMessage = `Password incorrect`
} else {
newPassword = params[1]
if newPassword == "*" {
newPassword = ""
}
}
}
errorMessage = nsConfirmPassword(server, target, params[0])
}
default:
errorMessage = `Invalid parameters`
@ -1422,6 +1534,52 @@ func suspensionToString(client *Client, suspension AccountSuspension) (result st
return fmt.Sprintf(client.t("Account %[1]s suspended at %[2]s. Duration: %[3]s. %[4]s"), suspension.AccountName, ts, duration, reason)
}
func nsSendpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if !nsLoginThrottleCheck(service, client, rb) {
return
}
account := params[0]
var message string
err := server.accounts.NsSendpass(client, account)
switch err {
case nil:
server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf("Client %s sent a password reset for account %s", client.Nick(), account))
message = `Successfully sent password reset email`
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
message = err.Error()
case errValidEmailRequired:
message = `That account is not associated with an email address`
case errLimitExceeded:
message = `Try again later`
default:
server.logger.Error("services", "error in NS SENDPASS", err.Error())
message = `An error occurred`
}
rb.Notice(client.t(message))
}
func nsResetpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if !nsLoginThrottleCheck(service, client, rb) {
return
}
var message string
err := server.accounts.NsResetpass(client, params[0], params[1], params[2])
switch err {
case nil:
message = `Successfully reset account password`
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended, errAccountBadPassphrase:
message = err.Error()
case errAccountInvalidCredentials:
message = `Code did not match`
default:
server.logger.Error("services", "error in NS RESETPASS", err.Error())
message = `An error occurred`
}
rb.Notice(client.t(message))
}
func nsRenameHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
oldName, newName := params[0], params[1]
err := server.accounts.Rename(oldName, newName)

@ -478,7 +478,8 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff
}
}
if client == target || oper.HasRoleCapab("ban") || !target.HasMode(modes.Cloaked) {
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", targetInfo.username, target.RawHostname()), target.IPString(), client.t("Actual user@host, Actual IP"))
ip, hostname := target.getWhoisActually()
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", targetInfo.username, hostname), utils.IPStringToHostname(ip.String()), client.t("Actual user@host, Actual IP"))
}
if client == target || oper.HasRoleCapab("samode") {
rb.Add(nil, client.server.name, RPL_WHOISMODES, cnick, tnick, fmt.Sprintf(client.t("is using modes +%s"), target.modes.String()))

@ -1,3 +1,4 @@
//go:build !plan9
// +build !plan9
// Copyright (c) 2020 Shivaram Lingamneni

@ -1,3 +1,4 @@
//go:build plan9
// +build plan9
// Copyright (c) 2020 Shivaram Lingamneni

@ -354,7 +354,14 @@ func SendMail(addr string, a Auth, heloDomain string, from string, to []string,
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
var config *tls.Config
if requireTLS {
config = &tls.Config{ServerName: c.serverName}
} else {
// if TLS isn't a hard requirement, don't verify the certificate either,
// since a MITM attacker could just remove the STARTTLS advertisement
config = &tls.Config{InsecureSkipVerify: true}
}
if testHookStartTLS != nil {
testHookStartTLS(config)
}

@ -1,3 +1,4 @@
//go:build linux
// +build linux
package utils

@ -1,3 +1,4 @@
//go:build !linux
// +build !linux
package utils

@ -387,6 +387,13 @@ accounts:
blacklist-regexes:
# - ".*@mailinator.com"
timeout: 60s
# email-based password reset:
password-reset:
enabled: false
# time before we allow resending the email
cooldown: 1h
# time for which a password reset code is valid
timeout: 1d
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)

@ -1,3 +0,0 @@
module github.com/go-sql-driver/mysql
go 1.10

@ -1,3 +0,0 @@
module github.com/gorilla/websocket
go 1.12

@ -1,3 +0,0 @@
module github.com/tidwall/btree
go 1.16

@ -1,12 +0,0 @@
module github.com/tidwall/buntdb
go 1.16
require (
github.com/tidwall/btree v0.6.0
github.com/tidwall/gjson v1.8.0
github.com/tidwall/grect v0.1.2
github.com/tidwall/lotsa v1.0.2
github.com/tidwall/match v1.0.3
github.com/tidwall/rtred v0.1.2
)

@ -1,16 +0,0 @@
github.com/tidwall/btree v0.6.0 h1:JLYAFGV+1gjyFi3iQbO/fupBin+Ooh7dxqVV0twJ1Bo=
github.com/tidwall/btree v0.6.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/grect v0.1.2 h1:wKVeQVZhjaFCKTTlpkDe3Ex4ko3cMGW3MRKawRe8uQ4=
github.com/tidwall/grect v0.1.2/go.mod h1:v+n4ewstPGduVJebcp5Eh2WXBJBumNzyhK8GZt4gHNw=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=

@ -1,8 +0,0 @@
module github.com/tidwall/gjson
go 1.12
require (
github.com/tidwall/match v1.0.3
github.com/tidwall/pretty v1.1.0
)

@ -1,4 +0,0 @@
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=

@ -1,5 +0,0 @@
module github.com/tidwall/grect
go 1.15
require github.com/tidwall/gjson v1.8.0

@ -1,6 +0,0 @@
github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=

@ -1,3 +0,0 @@
module github.com/tidwall/match
go 1.15

@ -1,5 +0,0 @@
module github.com/tidwall/rtred
go 1.15
require github.com/tidwall/tinyqueue v0.1.1

@ -1,2 +0,0 @@
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=

@ -1,3 +0,0 @@
module github.com/tidwall/tinyqueue
go 1.15

@ -1,3 +0,0 @@
module github.com/xdg-go/pbkdf2
go 1.9

@ -1,8 +0,0 @@
module github.com/xdg-go/scram
go 1.11
require (
github.com/xdg-go/pbkdf2 v1.0.0
github.com/xdg-go/stringprep v1.0.2
)

@ -1,7 +0,0 @@
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

5
vendor/golang.org/x/term/go.mod generated vendored

@ -1,5 +0,0 @@
module golang.org/x/term
go 1.11
require golang.org/x/sys v0.0.0-20201119102817-f84b799fce68

2
vendor/golang.org/x/term/go.sum generated vendored

@ -1,2 +0,0 @@
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

5
vendor/gopkg.in/yaml.v2/go.mod generated vendored

@ -1,5 +0,0 @@
module gopkg.in/yaml.v2
go 1.15
require gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405

30
vendor/modules.txt vendored

@ -17,74 +17,84 @@ github.com/ergochat/confusables
## explicit
github.com/ergochat/go-ident
# github.com/ergochat/irc-go v0.0.0-20210617222258-256f1601d3ce
## explicit
## explicit; go 1.15
github.com/ergochat/irc-go/ircfmt
github.com/ergochat/irc-go/ircmsg
github.com/ergochat/irc-go/ircreader
github.com/ergochat/irc-go/ircutils
# github.com/go-sql-driver/mysql v1.6.0
## explicit
## explicit; go 1.10
github.com/go-sql-driver/mysql
# github.com/go-test/deep v1.0.6
## explicit
## explicit; go 1.13
# github.com/golang-jwt/jwt v3.2.1+incompatible
## explicit
github.com/golang-jwt/jwt
# github.com/gorilla/websocket v1.4.2 => github.com/ergochat/websocket v1.4.2-oragono1
## explicit
## explicit; go 1.12
github.com/gorilla/websocket
# github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
## explicit
github.com/okzk/sdnotify
# github.com/onsi/ginkgo v1.12.0
## explicit
## explicit; go 1.12
# github.com/onsi/gomega v1.9.0
## explicit
# github.com/stretchr/testify v1.4.0
## explicit
# github.com/tidwall/btree v0.6.0
## explicit; go 1.16
github.com/tidwall/btree
# github.com/tidwall/buntdb v1.2.6
## explicit
## explicit; go 1.16
github.com/tidwall/buntdb
# github.com/tidwall/gjson v1.8.0
## explicit; go 1.12
github.com/tidwall/gjson
# github.com/tidwall/grect v0.1.2
## explicit; go 1.15
github.com/tidwall/grect
# github.com/tidwall/match v1.0.3
## explicit; go 1.15
github.com/tidwall/match
# github.com/tidwall/pretty v1.1.0
## explicit
github.com/tidwall/pretty
# github.com/tidwall/rtred v0.1.2
## explicit; go 1.15
github.com/tidwall/rtred
github.com/tidwall/rtred/base
# github.com/tidwall/tinyqueue v0.1.1
## explicit; go 1.15
github.com/tidwall/tinyqueue
# github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
## explicit
github.com/toorop/go-dkim
# github.com/xdg-go/pbkdf2 v1.0.0
## explicit; go 1.9
github.com/xdg-go/pbkdf2
# github.com/xdg-go/scram v1.0.2 => github.com/ergochat/scram v1.0.2-ergo1
## explicit
## explicit; go 1.11
github.com/xdg-go/scram
# golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
## explicit
## explicit; go 1.11
golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish
golang.org/x/crypto/pbkdf2
golang.org/x/crypto/sha3
golang.org/x/crypto/ssh/terminal
# golang.org/x/sys v0.0.0-20201119102817-f84b799fce68
## explicit; go 1.12
golang.org/x/sys/cpu
golang.org/x/sys/internal/unsafeheader
golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
# golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
## explicit; go 1.11
golang.org/x/term
# golang.org/x/text v0.3.6
## explicit
## explicit; go 1.11
golang.org/x/text/cases
golang.org/x/text/internal
golang.org/x/text/internal/language
@ -99,7 +109,7 @@ golang.org/x/text/unicode/bidi
golang.org/x/text/unicode/norm
golang.org/x/text/width
# gopkg.in/yaml.v2 v2.4.0
## explicit
## explicit; go 1.15
gopkg.in/yaml.v2
# github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
# github.com/xdg-go/scram => github.com/ergochat/scram v1.0.2-ergo1