This commit is contained in:
Shivaram Lingamneni
2020-04-05 03:48:59 -04:00
parent 6e630a0b5c
commit ee05a4324d
19 changed files with 1738 additions and 59 deletions
+23 -29
View File
@@ -4,9 +4,9 @@
package irc
import (
"bytes"
"encoding/json"
"fmt"
"net/smtp"
"strconv"
"strings"
"sync"
@@ -16,6 +16,7 @@ import (
"github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/email"
"github.com/oragono/oragono/irc/ldap"
"github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/utils"
@@ -483,7 +484,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
return err
}
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
if err != nil {
am.Unregister(casefoldedAccount, true)
return errCallbackFailed
@@ -698,17 +699,17 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
return err
}
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
func (am *AccountManager) dispatchCallback(client *Client, account string, callbackNamespace string, callbackValue string) (string, error) {
if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
return "", nil
} else if callbackNamespace == "mailto" {
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
return am.dispatchMailtoCallback(client, account, callbackValue)
} else {
return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
}
}
func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) {
config := am.server.Config().Accounts.Registration.Callbacks.Mailto
code = utils.GenerateSecretToken()
@@ -716,34 +717,27 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
if subject == "" {
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
}
messageStrings := []string{
fmt.Sprintf("From: %s\r\n", config.Sender),
fmt.Sprintf("To: %s\r\n", callbackValue),
fmt.Sprintf("Subject: %s\r\n", subject),
"\r\n", // end headers, begin message body
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
"\r\n",
client.t("To verify your account, issue the following command:") + "\r\n",
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
}
var message []byte
for i := 0; i < len(messageStrings); i++ {
message = append(message, []byte(messageStrings[i])...)
}
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
var auth smtp.Auth
if config.Username != "" && config.Password != "" {
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
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, "Subject: %s\r\n", subject)
message.WriteString("\r\n") // blank line: end headers, begin message body
fmt.Fprintf(&message, client.t("Account: %s"), account)
message.WriteString("\r\n")
fmt.Fprintf(&message, client.t("Verification code: %s"), code)
message.WriteString("\r\n")
message.WriteString("\r\n")
message.WriteString(client.t("To verify your account, issue the following command:"))
message.WriteString("\r\n")
fmt.Fprintf(&message, "/MSG NickServ VERIFY %s %s\r\n", account, code)
// TODO: this will never send the password in plaintext over a nonlocal link,
// but it might send the email in plaintext, regardless of the value of
// config.TLS.InsecureSkipVerify
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
err = email.SendMail(config, callbackValue, message.Bytes())
if err != nil {
am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
am.server.logger.Error("internal", "Failed to dispatch e-mail to", callbackValue, err.Error())
}
return
}
+12 -14
View File
@@ -24,6 +24,7 @@ import (
"github.com/oragono/oragono/irc/cloaks"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/email"
"github.com/oragono/oragono/irc/isupport"
"github.com/oragono/oragono/irc/languages"
"github.com/oragono/oragono/irc/ldap"
@@ -290,20 +291,7 @@ type AccountRegistrationConfig struct {
EnabledCredentialTypes []string `yaml:"-"`
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
Callbacks struct {
Mailto struct {
Server string
Port int
TLS struct {
Enabled bool
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
ServerName string `yaml:"servername"`
}
Username string
Password string
Sender string
VerifyMessageSubject string `yaml:"verify-message-subject"`
VerifyMessage string `yaml:"verify-message"`
}
Mailto email.MailtoConfig
}
BcryptCost uint `yaml:"bcrypt-cost"`
}
@@ -975,14 +963,24 @@ func LoadConfig(filename string) (config *Config, err error) {
// hardcode this for now
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
mailtoEnabled := false
for i, name := range config.Accounts.Registration.EnabledCallbacks {
if name == "none" {
// we store "none" as "*" internally
config.Accounts.Registration.EnabledCallbacks[i] = "*"
} else if name == "mailto" {
mailtoEnabled = true
}
}
sort.Strings(config.Accounts.Registration.EnabledCallbacks)
if mailtoEnabled {
err := config.Accounts.Registration.Callbacks.Mailto.Postprocess(config.Server.Name)
if err != nil {
return nil, err
}
}
config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
if err != nil {
return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package email
import (
"errors"
dkim "github.com/toorop/go-dkim"
"io/ioutil"
)
var (
ErrMissingFields = errors.New("DKIM config is missing fields")
)
type DKIMConfig struct {
Domain string
Selector string
KeyFile string `yaml:"key-file"`
keyBytes []byte
}
func (dkim *DKIMConfig) Postprocess() (err error) {
if dkim.Domain != "" {
if dkim.Selector == "" || dkim.KeyFile == "" {
return ErrMissingFields
}
dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
if err != nil {
return err
}
}
return nil
}
var defaultOptions = dkim.SigOptions{
Version: 1,
Canonicalization: "relaxed/relaxed",
Algo: "rsa-sha256",
Headers: []string{"from", "to", "subject", "message-id"},
BodyLength: 0,
QueryMethods: []string{"dns/txt"},
AddSignatureTimestamp: true,
SignatureExpireIn: 0,
}
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
options := defaultOptions
options.PrivateKey = dkimConfig.keyBytes
options.Domain = dkimConfig.Domain
options.Selector = dkimConfig.Selector
err = dkim.Sign(&message, options)
return message, err
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package email
import (
"errors"
"fmt"
"net"
"regexp"
"strings"
"github.com/oragono/oragono/irc/smtp"
)
var (
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
ErrInvalidAddress = errors.New("Email address is blacklisted")
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
)
type MTAConfig struct {
Server string
Port int
Username string
Password string
}
type MailtoConfig struct {
// legacy config format assumed the use of an MTA/smarthost,
// so server, port, etc. appear directly at top level
// XXX: see https://github.com/go-yaml/yaml/issues/63
MTAConfig `yaml:",inline"`
Sender string
HeloDomain string `yaml:"helo-domain"`
RequireTLS bool `yaml:"require-tls"`
VerifyMessageSubject string `yaml:"verify-message-subject"`
DKIM DKIMConfig
MTAReal MTAConfig `yaml:"mta"`
BlacklistRegexes []string `yaml:"blacklist-regexes"`
blacklistRegexes []*regexp.Regexp
}
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
if config.Sender == "" {
return errors.New("Invalid mailto sender address")
}
// check for MTA config fields at top level,
// copy to MTAReal if present
if config.Server != "" && config.MTAReal.Server == "" {
config.MTAReal = config.MTAConfig
}
if config.HeloDomain == "" {
config.HeloDomain = heloDomain
}
for _, reg := range config.BlacklistRegexes {
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
if err != nil {
return err
}
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
}
if config.MTAConfig.Server != "" {
// smarthost, nothing more to validate
return nil
}
return config.DKIM.Postprocess()
}
// get the preferred MX record hostname, "" on error
func lookupMX(domain string) (server string) {
var minPref uint16
results, err := net.LookupMX(domain)
if err != nil {
return
}
for _, result := range results {
if minPref == 0 || result.Pref < minPref {
server, minPref = result.Host, result.Pref
}
}
return
}
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
for _, reg := range config.blacklistRegexes {
if reg.MatchString(recipient) {
return ErrBlacklistedAddress
}
}
if config.DKIM.Domain != "" {
msg, err = DKIMSign(msg, config.DKIM)
if err != nil {
return
}
}
var addr string
var auth smtp.Auth
if config.MTAReal.Server != "" {
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
}
} else {
idx := strings.IndexByte(recipient, '@')
if idx == -1 {
return ErrInvalidAddress
}
mx := lookupMX(recipient[idx+1:])
if mx == "" {
return ErrNoMXRecord
}
addr = fmt.Sprintf("%s:smtp", mx)
}
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
}
+5 -2
View File
@@ -316,7 +316,8 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
// attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
// XXX: modified in Oragono to add `requireTLS` and `heloDomain` arguments
func SendMail(addr string, a Auth, heloDomain string, from string, to []string, msg []byte, requireTLS bool) error {
if err := validateLine(from); err != nil {
return err
}
@@ -330,7 +331,7 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
return err
}
defer c.Close()
if err = c.hello(); err != nil {
if err = c.Hello(heloDomain); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
@@ -341,6 +342,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {
if err = c.StartTLS(config); err != nil {
return err
}
} else if requireTLS {
return errors.New("TLS required, but not negotiated")
}
if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok {