mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-03 07:13:58 -07:00
+23
-29
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user