|
|
|
@ -14,11 +14,69 @@
|
|
|
|
|
package smtp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// TestServerProto is the protocol used for the simple SMTP test server
|
|
|
|
|
TestServerProto = "tcp"
|
|
|
|
|
// TestServerAddr is the address the simple SMTP test server listens on
|
|
|
|
|
TestServerAddr = "127.0.0.1"
|
|
|
|
|
// TestServerPortBase is the base port for the simple SMTP test server
|
|
|
|
|
TestServerPortBase = 12025
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
|
|
|
|
|
var PortAdder atomic.Int32
|
|
|
|
|
|
|
|
|
|
// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
|
|
|
|
|
//
|
|
|
|
|
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
|
|
|
|
|
// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
|
|
|
|
var localhostCert = []byte(`
|
|
|
|
|
-----BEGIN CERTIFICATE-----
|
|
|
|
|
MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
|
|
|
|
|
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
|
|
|
|
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
|
|
|
|
|
gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa
|
|
|
|
|
JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30
|
|
|
|
|
LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw
|
|
|
|
|
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
|
|
|
|
|
MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
|
|
|
|
|
AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi
|
|
|
|
|
NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4
|
|
|
|
|
n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF
|
|
|
|
|
tN8URjVmyEo=
|
|
|
|
|
-----END CERTIFICATE-----`)
|
|
|
|
|
|
|
|
|
|
// localhostKey is the private key for localhostCert.
|
|
|
|
|
var localhostKey = []byte(testingKey(`
|
|
|
|
|
-----BEGIN RSA TESTING KEY-----
|
|
|
|
|
MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h
|
|
|
|
|
AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu
|
|
|
|
|
lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB
|
|
|
|
|
AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA
|
|
|
|
|
kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM
|
|
|
|
|
VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m
|
|
|
|
|
542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb
|
|
|
|
|
PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2
|
|
|
|
|
6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB
|
|
|
|
|
vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP
|
|
|
|
|
QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i
|
|
|
|
|
jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c
|
|
|
|
|
qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg==
|
|
|
|
|
-----END RSA TESTING KEY-----`))
|
|
|
|
|
|
|
|
|
|
type authTest struct {
|
|
|
|
|
auth Auth
|
|
|
|
|
challenges []string
|
|
|
|
@ -302,6 +360,59 @@ func TestPlainAuth(t *testing.T) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("PLAIN authentication on test server", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := PlainAuth("", "user", "pass", TestServerAddr, false)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err != nil {
|
|
|
|
|
t.Errorf("failed to authenticate to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("PLAIN authentication on test server should fail", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FailOnAuth: true,
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := PlainAuth("", "user", "pass", TestServerAddr, false)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err == nil {
|
|
|
|
|
t.Errorf("expected authentication to fail")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlainAuth_noEnc(t *testing.T) {
|
|
|
|
@ -397,100 +508,350 @@ func TestPlainAuth_noEnc(t *testing.T) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("PLAIN-NOENC authentication on test server", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := PlainAuth("", "user", "pass", TestServerAddr, true)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err != nil {
|
|
|
|
|
t.Errorf("failed to authenticate to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("PLAIN-NOENC authentication on test server should fail", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FailOnAuth: true,
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := PlainAuth("", "user", "pass", TestServerAddr, true)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err == nil {
|
|
|
|
|
t.Errorf("expected authentication to fail")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoginAuth(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
authName string
|
|
|
|
|
server *ServerInfo
|
|
|
|
|
shouldFail bool
|
|
|
|
|
wantErr error
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN auth succeeds",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", TLS: true},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// OK to use PlainAuth on localhost without TLS
|
|
|
|
|
name: "LOGIN on localhost is allowed to go unencrypted",
|
|
|
|
|
authName: "localhost",
|
|
|
|
|
server: &ServerInfo{Name: "localhost", TLS: false},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// NOT OK on non-localhost, even if server says LOGIN is OK.
|
|
|
|
|
// (We don't know that the server is the real server.)
|
|
|
|
|
name: "LOGIN on non-localhost is not allowed to go unencrypted",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
|
|
|
|
shouldFail: true,
|
|
|
|
|
wantErr: ErrUnencrypted,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN on non-localhost with no LOGIN announcement, is not allowed to go unencrypted",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
|
|
|
|
shouldFail: true,
|
|
|
|
|
wantErr: ErrUnencrypted,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN with wrong hostname",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
|
|
|
|
shouldFail: true,
|
|
|
|
|
wantErr: ErrWrongHostname,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
user := "toni.tester@example.com"
|
|
|
|
|
pass := "v3ryS3Cur3P4ssw0rd"
|
|
|
|
|
auth := LoginAuth(user, pass, tt.authName, false)
|
|
|
|
|
method, _, err := auth.Start(tt.server)
|
|
|
|
|
if err != nil && !tt.shouldFail {
|
|
|
|
|
t.Errorf("login authentication failed: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err == nil && tt.shouldFail {
|
|
|
|
|
t.Error("login authentication was expected to fail")
|
|
|
|
|
}
|
|
|
|
|
if tt.wantErr != nil {
|
|
|
|
|
if !errors.Is(err, tt.wantErr) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if method != "LOGIN" {
|
|
|
|
|
t.Errorf("expected method return to be: %q, got: %q", "LOGIN", method)
|
|
|
|
|
}
|
|
|
|
|
resp, err := auth.Next([]byte(user), true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("failed on first server challange: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal([]byte(user), resp) {
|
|
|
|
|
t.Errorf("expected response to first challange to be: %q, got: %q", user, resp)
|
|
|
|
|
}
|
|
|
|
|
resp, err = auth.Next([]byte(pass), true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("failed on second server challange: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal([]byte(pass), resp) {
|
|
|
|
|
t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp)
|
|
|
|
|
}
|
|
|
|
|
resp, err = auth.Next([]byte("nonsense"), true)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("expected third server challange to fail, but didn't")
|
|
|
|
|
}
|
|
|
|
|
if !errors.Is(err, ErrUnexpectedServerResponse) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerResponse, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
t.Run("LOGIN authentication on test server", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := LoginAuth("user", "pass", TestServerAddr, false)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err != nil {
|
|
|
|
|
t.Errorf("failed to authenticate to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("LOGIN authentication on test server should fail", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FailOnAuth: true,
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := LoginAuth("user", "pass", TestServerAddr, false)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err == nil {
|
|
|
|
|
t.Errorf("expected authentication to fail")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoginAuth_noEnc(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
authName string
|
|
|
|
|
server *ServerInfo
|
|
|
|
|
shouldFail bool
|
|
|
|
|
wantErr error
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN-NOENC auth succeeds",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", TLS: true},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// OK to use PlainAuth on localhost without TLS
|
|
|
|
|
name: "LOGIN-NOENC on localhost is allowed to go unencrypted",
|
|
|
|
|
authName: "localhost",
|
|
|
|
|
server: &ServerInfo{Name: "localhost", TLS: false},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// ALSO OK on non-localhost. This auth mode is specificly for that.
|
|
|
|
|
name: "LOGIN-NOENC on non-localhost is allowed to go unencrypted",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN-NOENC on non-localhost with no LOGIN announcement, is not allowed to go unencrypted",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
|
|
|
|
shouldFail: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "LOGIN-NOENC with wrong hostname",
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
|
|
|
|
shouldFail: true,
|
|
|
|
|
wantErr: ErrWrongHostname,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
user := "toni.tester@example.com"
|
|
|
|
|
pass := "v3ryS3Cur3P4ssw0rd"
|
|
|
|
|
auth := LoginAuth(user, pass, tt.authName, true)
|
|
|
|
|
method, _, err := auth.Start(tt.server)
|
|
|
|
|
if err != nil && !tt.shouldFail {
|
|
|
|
|
t.Errorf("login authentication failed: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err == nil && tt.shouldFail {
|
|
|
|
|
t.Error("login authentication was expected to fail")
|
|
|
|
|
}
|
|
|
|
|
if tt.wantErr != nil {
|
|
|
|
|
if !errors.Is(err, tt.wantErr) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if method != "LOGIN" {
|
|
|
|
|
t.Errorf("expected method return to be: %q, got: %q", "LOGIN", method)
|
|
|
|
|
}
|
|
|
|
|
resp, err := auth.Next([]byte(user), true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("failed on first server challange: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal([]byte(user), resp) {
|
|
|
|
|
t.Errorf("expected response to first challange to be: %q, got: %q", user, resp)
|
|
|
|
|
}
|
|
|
|
|
resp, err = auth.Next([]byte(pass), true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("failed on second server challange: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal([]byte(pass), resp) {
|
|
|
|
|
t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp)
|
|
|
|
|
}
|
|
|
|
|
resp, err = auth.Next([]byte("nonsense"), true)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("expected third server challange to fail, but didn't")
|
|
|
|
|
}
|
|
|
|
|
if !errors.Is(err, ErrUnexpectedServerResponse) {
|
|
|
|
|
t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerResponse, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
t.Run("LOGIN-NOENC authentication on test server", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := LoginAuth("user", "pass", TestServerAddr, true)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err != nil {
|
|
|
|
|
t.Errorf("failed to authenticate to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("LOGIN-NOENC authentication on test server should fail", func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
PortAdder.Add(1)
|
|
|
|
|
serverPort := int(TestServerPortBase + PortAdder.Load())
|
|
|
|
|
featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
|
|
|
|
|
go func() {
|
|
|
|
|
if err := simpleSMTPServer(ctx, t, &serverProps{
|
|
|
|
|
FailOnAuth: true,
|
|
|
|
|
FeatureSet: featureSet,
|
|
|
|
|
ListenPort: serverPort},
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Errorf("failed to start test server: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
time.Sleep(time.Millisecond * 30)
|
|
|
|
|
|
|
|
|
|
auth := LoginAuth("user", "pass", TestServerAddr, true)
|
|
|
|
|
client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to connect to test server: %s", err)
|
|
|
|
|
}
|
|
|
|
|
if err = client.Auth(auth); err == nil {
|
|
|
|
|
t.Errorf("expected authentication to fail")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func TestAuthLogin(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
authName string
|
|
|
|
|
server *ServerInfo
|
|
|
|
|
err string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", TLS: true},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// OK to use LoginAuth on localhost without TLS
|
|
|
|
|
authName: "localhost",
|
|
|
|
|
server: &ServerInfo{Name: "localhost", TLS: false},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// NOT OK on non-localhost, even if server says PLAIN is OK.
|
|
|
|
|
// (We don't know that the server is the real server.)
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
|
|
|
|
err: "unencrypted connection",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
|
|
|
|
err: "unencrypted connection",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
|
|
|
|
err: "wrong host name",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for i, tt := range tests {
|
|
|
|
|
auth := LoginAuth("foo", "bar", tt.authName, false)
|
|
|
|
|
_, _, err := auth.Start(tt.server)
|
|
|
|
|
got := ""
|
|
|
|
|
if err != nil {
|
|
|
|
|
got = err.Error()
|
|
|
|
|
}
|
|
|
|
|
if got != tt.err {
|
|
|
|
|
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAuthLoginNoEnc(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
authName string
|
|
|
|
|
server *ServerInfo
|
|
|
|
|
err string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", TLS: true},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// OK to use LoginAuth on localhost without TLS
|
|
|
|
|
authName: "localhost",
|
|
|
|
|
server: &ServerInfo{Name: "localhost", TLS: false},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow
|
|
|
|
|
// non-encrypted connections.
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
authName: "servername",
|
|
|
|
|
server: &ServerInfo{Name: "attacker", TLS: true},
|
|
|
|
|
err: "wrong host name",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for i, tt := range tests {
|
|
|
|
|
auth := LoginAuth("foo", "bar", tt.authName, true)
|
|
|
|
|
_, _, err := auth.Start(tt.server)
|
|
|
|
|
got := ""
|
|
|
|
|
if err != nil {
|
|
|
|
|
got = err.Error()
|
|
|
|
|
}
|
|
|
|
|
if got != tt.err {
|
|
|
|
|
t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestXOAuth2OK(t *testing.T) {
|
|
|
|
|
server := []string{
|
|
|
|
@ -2655,3 +3016,229 @@ func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
// testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s.
|
|
|
|
|
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
|
|
|
|
|
|
|
|
|
// serverProps represents the configuration properties for the SMTP server.
|
|
|
|
|
type serverProps struct {
|
|
|
|
|
FailOnAuth bool
|
|
|
|
|
FailOnDataInit bool
|
|
|
|
|
FailOnDataClose bool
|
|
|
|
|
FailOnHelo bool
|
|
|
|
|
FailOnMailFrom bool
|
|
|
|
|
FailOnNoop bool
|
|
|
|
|
FailOnQuit bool
|
|
|
|
|
FailOnReset bool
|
|
|
|
|
FailOnSTARTTLS bool
|
|
|
|
|
FailTemp bool
|
|
|
|
|
FeatureSet string
|
|
|
|
|
ListenPort int
|
|
|
|
|
SSLListener bool
|
|
|
|
|
IsTLS bool
|
|
|
|
|
SupportDSN bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
|
|
|
|
|
// The provided featureSet represents in what the server responds to EHLO command
|
|
|
|
|
// failReset controls if a RSET succeeds
|
|
|
|
|
func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) error {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if props == nil {
|
|
|
|
|
return fmt.Errorf("no server properties provided")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var listener net.Listener
|
|
|
|
|
var err error
|
|
|
|
|
if props.SSLListener {
|
|
|
|
|
keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to read TLS keypair: %w", err)
|
|
|
|
|
}
|
|
|
|
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
|
|
|
|
listener, err = tls.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort),
|
|
|
|
|
tlsConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create TLS listener: %s", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
listener, err = net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort))
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("unable to listen on %s://%s: %w (SSL: %t)", TestServerProto, TestServerAddr, err,
|
|
|
|
|
props.SSLListener)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
if err := listener.Close(); err != nil {
|
|
|
|
|
t.Logf("failed to close listener: %s", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
|
|
|
|
connection, err := listener.Accept()
|
|
|
|
|
var opErr *net.OpError
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.As(err, &opErr) && opErr.Temporary() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("unable to accept connection: %w", err)
|
|
|
|
|
}
|
|
|
|
|
handleTestServerConnection(connection, t, props)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleTestServerConnection(connection net.Conn, t *testing.T, props *serverProps) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if !props.IsTLS {
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
if err := connection.Close(); err != nil {
|
|
|
|
|
t.Logf("failed to close connection: %s", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reader := bufio.NewReader(connection)
|
|
|
|
|
writer := bufio.NewWriter(connection)
|
|
|
|
|
|
|
|
|
|
writeLine := func(data string) {
|
|
|
|
|
_, err := writer.WriteString(data + "\r\n")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Logf("failed to write line: %s", err)
|
|
|
|
|
}
|
|
|
|
|
_ = writer.Flush()
|
|
|
|
|
}
|
|
|
|
|
writeOK := func() {
|
|
|
|
|
writeLine("250 2.0.0 OK")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !props.IsTLS {
|
|
|
|
|
writeLine("220 go-mail test server ready ESMTP")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
data, err := reader.ReadString('\n')
|
|
|
|
|
if err != nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(time.Millisecond)
|
|
|
|
|
|
|
|
|
|
var datastring string
|
|
|
|
|
data = strings.TrimSpace(data)
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"):
|
|
|
|
|
if len(strings.Split(data, " ")) != 2 {
|
|
|
|
|
writeLine("501 Syntax: EHLO hostname")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if props.FailOnHelo {
|
|
|
|
|
writeLine("500 5.5.2 Error: fail on HELO")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("250-localhost.localdomain\r\n" + props.FeatureSet)
|
|
|
|
|
case strings.HasPrefix(data, "MAIL FROM:"):
|
|
|
|
|
if props.FailOnMailFrom {
|
|
|
|
|
writeLine("500 5.5.2 Error: fail on MAIL FROM")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
from := strings.TrimPrefix(data, "MAIL FROM:")
|
|
|
|
|
from = strings.ReplaceAll(from, "BODY=8BITMIME", "")
|
|
|
|
|
from = strings.ReplaceAll(from, "SMTPUTF8", "")
|
|
|
|
|
if props.SupportDSN {
|
|
|
|
|
from = strings.ReplaceAll(from, "RET=FULL", "")
|
|
|
|
|
}
|
|
|
|
|
from = strings.TrimSpace(from)
|
|
|
|
|
if !strings.EqualFold(from, "<valid-from@domain.tld>") {
|
|
|
|
|
writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from))
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeOK()
|
|
|
|
|
case strings.HasPrefix(data, "RCPT TO:"):
|
|
|
|
|
to := strings.TrimPrefix(data, "RCPT TO:")
|
|
|
|
|
if props.SupportDSN {
|
|
|
|
|
to = strings.ReplaceAll(to, "NOTIFY=FAILURE,SUCCESS", "")
|
|
|
|
|
}
|
|
|
|
|
to = strings.TrimSpace(to)
|
|
|
|
|
if !strings.EqualFold(to, "<valid-to@domain.tld>") {
|
|
|
|
|
writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to))
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeOK()
|
|
|
|
|
case strings.HasPrefix(data, "AUTH"):
|
|
|
|
|
if props.FailOnAuth {
|
|
|
|
|
writeLine("535 5.7.8 Error: authentication failed")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("235 2.7.0 Authentication successful")
|
|
|
|
|
case strings.EqualFold(data, "DATA"):
|
|
|
|
|
if props.FailOnDataInit {
|
|
|
|
|
writeLine("503 5.5.1 Error: fail on DATA init")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("354 End data with <CR><LF>.<CR><LF>")
|
|
|
|
|
for {
|
|
|
|
|
ddata, derr := reader.ReadString('\n')
|
|
|
|
|
if derr != nil {
|
|
|
|
|
t.Logf("failed to read data from connection: %s", derr)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
ddata = strings.TrimSpace(ddata)
|
|
|
|
|
if ddata == "." {
|
|
|
|
|
if props.FailOnDataClose {
|
|
|
|
|
writeLine("500 5.0.0 Error during DATA transmission")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if props.FailTemp {
|
|
|
|
|
writeLine("451 4.3.0 Error: fail on DATA close")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("250 2.0.0 Ok: queued as 1234567890")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
datastring += ddata + "\n"
|
|
|
|
|
}
|
|
|
|
|
case strings.EqualFold(data, "noop"):
|
|
|
|
|
if props.FailOnNoop {
|
|
|
|
|
writeLine("500 5.0.0 Error: fail on NOOP")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeOK()
|
|
|
|
|
case strings.EqualFold(data, "vrfy"):
|
|
|
|
|
writeOK()
|
|
|
|
|
case strings.EqualFold(data, "rset"):
|
|
|
|
|
if props.FailOnReset {
|
|
|
|
|
writeLine("500 5.1.2 Error: reset failed")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeOK()
|
|
|
|
|
case strings.EqualFold(data, "quit"):
|
|
|
|
|
if props.FailOnQuit {
|
|
|
|
|
writeLine("500 5.1.2 Error: quit failed")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("221 2.0.0 Bye")
|
|
|
|
|
return
|
|
|
|
|
case strings.EqualFold(data, "starttls"):
|
|
|
|
|
if props.FailOnSTARTTLS {
|
|
|
|
|
writeLine("500 5.1.2 Error: starttls failed")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeLine("500 5.1.2 Error: starttls failed - " + err.Error())
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
writeLine("220 Ready to start TLS")
|
|
|
|
|
tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}}
|
|
|
|
|
connection = tls.Server(connection, tlsConfig)
|
|
|
|
|
props.IsTLS = true
|
|
|
|
|
handleTestServerConnection(connection, t, props)
|
|
|
|
|
default:
|
|
|
|
|
writeLine("500 5.5.2 Error: bad syntax")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|