From 5cdde922f63c5b677602d7e56ca408facd3234a7 Mon Sep 17 00:00:00 2001 From: Aine Date: Mon, 6 Oct 2025 11:21:24 +0100 Subject: [PATCH] add LOGIN SMTP auth method (do not use it, unless you have to!) --- internal/smtp/authserver.go | 68 ++++++++++++++++++++++++++++++++++++- internal/smtp/session.go | 22 +++++++++--- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/internal/smtp/authserver.go b/internal/smtp/authserver.go index a81f08c..b85327f 100644 --- a/internal/smtp/authserver.go +++ b/internal/smtp/authserver.go @@ -9,8 +9,73 @@ import ( "github.com/emersion/go-smtp" ) +const ( + loginStateNotStarted = iota + loginStateUsername + loginStatePassword +) + // ensure that PlainAuthServer implements sasl.Server -var _ sasl.Server = (*PlainAuthServer)(nil) +var ( + _ sasl.Server = (*PlainAuthServer)(nil) + _ sasl.Server = (*LoginAuthServer)(nil) +) + +// LoginAuthServer is a server implementation of the LOGIN authentication mechanism. +// It's a modified implementation of the go-sasl's original loginServer (MIT License), +// removed from go-sasl in https://github.com/emersion/go-sasl/commit/b788ff22d5a6b3970cde181998f52658a475bffc +// DO NOT USE IT, unless you have no other choice. +type LoginAuthServer struct { + done bool + state int + ctx context.Context //nolint:containedctx // that's per-request structure + bot matrixbot + conn *smtp.Conn + authenticate sasl.PlainAuthenticator // we use the same interface as PLAIN auth for simplicity + username string + password string +} + +// NewLoginAuthServer creates a new LOGIN authentication server. +func NewLoginAuthServer(ctx context.Context, bot matrixbot, conn *smtp.Conn, auth sasl.PlainAuthenticator) *LoginAuthServer { + return &LoginAuthServer{ + ctx: ctx, + bot: bot, + conn: conn, + authenticate: auth, + state: loginStateNotStarted, + } +} + +// Next processes the next step of the authentication. +func (a *LoginAuthServer) Next(response []byte) (challenge []byte, done bool, err error) { + if a.done { + err = sasl.ErrUnexpectedClientResponse + return challenge, done, err + } + + switch a.state { + case loginStateNotStarted: + a.state = loginStateUsername + return []byte("Username:"), false, nil + + case loginStateUsername: + a.username = string(response) + a.state = loginStatePassword + return []byte("Password:"), false, nil + + case loginStatePassword: + a.password = string(response) + a.done = true + err = a.authenticate("", a.username, a.password) + done = true + return challenge, done, err + + default: + err = sasl.ErrUnexpectedClientResponse + return challenge, done, err + } +} // PlainAuthServer is a server implementation of the PLAIN authentication mechanism. // It's a modified version of the original plainServer from https://github.com/emersion/go-sasl package (MIT License) @@ -35,6 +100,7 @@ func NewPlainAuthServer(ctx context.Context, bot matrixbot, conn *smtp.Conn, aut } } +// Next processes the next step of the authentication. func (a *PlainAuthServer) Next(response []byte) (challenge []byte, done bool, err error) { if a.done { err = sasl.ErrUnexpectedClientResponse diff --git a/internal/smtp/session.go b/internal/smtp/session.go index 1b2a061..a675941 100644 --- a/internal/smtp/session.go +++ b/internal/smtp/session.go @@ -48,20 +48,32 @@ type session struct { // AuthMechanisms returns the list of supported authentication mechanisms func (s *session) AuthMechanisms() []string { - return []string{sasl.Plain} + return []string{sasl.Plain, sasl.Login} } func (s *session) Auth(mech string) (sasl.Server, error) { + addr := s.conn.Conn().RemoteAddr() if !slices.Contains(s.AuthMechanisms(), mech) { - addr := s.conn.Conn().RemoteAddr() s.log.Info().Str("addr", addr.String()).Msg("banning due to invalid auth mechanism") s.bot.BanAuth(s.ctx, addr) return nil, ErrBanned } - return NewPlainAuthServer(s.ctx, s.bot, s.conn, func(identity, username, password string) error { - return s.authPlain(identity, username, password) - }), nil + switch mech { + case sasl.Login: + return NewLoginAuthServer(s.ctx, s.bot, s.conn, func(identity, username, password string) error { + return s.authPlain(identity, username, password) + }), nil + + case sasl.Plain: + return NewPlainAuthServer(s.ctx, s.bot, s.conn, func(identity, username, password string) error { + return s.authPlain(identity, username, password) + }), nil + default: + s.log.Warn().Str("addr", addr.String()).Str("mech", mech).Msg("unsupported auth mechanism") + s.bot.BanAuth(s.ctx, addr) + return nil, ErrBanned + } } func (s *session) authPlain(_, username, password string) error {