1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00

Add support for AUTH, closes #62

* Add PLAIN and LOGIN auth support
This commit is contained in:
Timur Makarchuk
2021-04-10 23:45:09 +03:00
committed by James Hillyerd
parent 3c5960aba0
commit c096f018d6
2 changed files with 113 additions and 0 deletions

View File

@@ -29,12 +29,25 @@ const (
GREET State = iota GREET State = iota
// READY State: Got HELO, waiting for MAIL // READY State: Got HELO, waiting for MAIL
READY READY
// LOGIN State: Got AUTH LOGIN command, expecting Username
LOGIN
// PASSWORD State: Got Username, expecting password
PASSWORD
// MAIL State: Got MAIL, accepting RCPTs // MAIL State: Got MAIL, accepting RCPTs
MAIL MAIL
// DATA State: Got DATA, waiting for "." // DATA State: Got DATA, waiting for "."
DATA DATA
// QUIT State: Client requested end of session // QUIT State: Client requested end of session
QUIT QUIT
// Messages sent to user during LOGIN auth procedure
// Can vary, but values are taken directly from spec
// https://tools.ietf.org/html/draft-murchison-sasl-login-00
//usernameChallenge sent when inviting user to provide username. Is base64 encoded string `User Name`
usernameChallenge = "VXNlciBOYW1lAA=="
//passwordChallenge sent when inviting user to provide password. Is base64 encoded string `Password`
passwordChallenge = "UGFzc3dvcmQA"
) )
// fromRegex captures the from address and optional BODY=8BITMIME clause. Matches FROM, while // fromRegex captures the from address and optional BODY=8BITMIME clause. Matches FROM, while
@@ -76,6 +89,7 @@ var commands = map[string]bool{
"QUIT": true, "QUIT": true,
"TURN": true, "TURN": true,
"STARTTLS": true, "STARTTLS": true,
"AUTH": true,
} }
// Session holds the state of an SMTP session // Session holds the state of an SMTP session
@@ -153,6 +167,16 @@ func (s *Server) startSession(id int, conn net.Conn) {
} }
line, err := ssn.readLine() line, err := ssn.readLine()
if err == nil { if err == nil {
//Handle LOGIN/PASSWORD states here, because they don't expect a command
switch ssn.state {
case LOGIN:
ssn.loginHandler(line)
continue
case PASSWORD:
ssn.passwordHandler(line)
continue
}
if cmd, arg, ok := ssn.parseCmd(line); ok { if cmd, arg, ok := ssn.parseCmd(line); ok {
// Check against valid SMTP commands // Check against valid SMTP commands
if cmd == "" { if cmd == "" {
@@ -260,6 +284,7 @@ func (s *Session) greetHandler(cmd string, arg string) {
// features before SIZE per RFC // features before SIZE per RFC
s.send("250-" + readyBanner) s.send("250-" + readyBanner)
s.send("250-8BITMIME") s.send("250-8BITMIME")
s.send("250-AUTH PLAIN LOGIN")
if s.Server.config.TLSEnabled && s.Server.tlsConfig != nil && s.tlsState == nil { if s.Server.config.TLSEnabled && s.Server.tlsConfig != nil && s.tlsState == nil {
s.send("250-STARTTLS") s.send("250-STARTTLS")
} }
@@ -281,7 +306,28 @@ func parseHelloArgument(arg string) (string, error) {
return domain, nil return domain, nil
} }
func (s *Session) loginHandler(line string) {
if len(line) == 0 {
s.send("500 invalid Username")
s.enterState(READY)
return
}
s.send(fmt.Sprintf("334 %v", passwordChallenge))
s.enterState(PASSWORD)
}
func (s *Session) passwordHandler(line string) {
if len(line) == 0 {
s.send("500 invalid Password")
s.enterState(READY)
return
}
s.send("235 Authentication successful")
s.enterState(READY)
}
// READY state -> waiting for MAIL // READY state -> waiting for MAIL
// AUTH can change
func (s *Session) readyHandler(cmd string, arg string) { func (s *Session) readyHandler(cmd string, arg string) {
if cmd == "STARTTLS" { if cmd == "STARTTLS" {
if !s.Server.config.TLSEnabled { if !s.Server.config.TLSEnabled {
@@ -305,6 +351,33 @@ func (s *Session) readyHandler(cmd string, arg string) {
s.tlsState = new(tls.ConnectionState) s.tlsState = new(tls.ConnectionState)
*s.tlsState = tlsConn.ConnectionState() *s.tlsState = tlsConn.ConnectionState()
s.enterState(GREET) s.enterState(GREET)
} else if cmd == "AUTH" {
args := strings.SplitN(arg, " ", 3)
authMethod := args[0]
switch authMethod {
case "PLAIN":
{
if len(args) != 2 {
s.send("500 Bad auth arguments")
s.logger.Warn().Msgf("Bad auth attempt: %q", arg)
return
}
s.logger.Info().Msgf("Accepting credentials: %q", args[1])
s.send("235 2.7.0 Authentication successful")
return
}
case "LOGIN":
{
s.send(fmt.Sprintf("334 %v", usernameChallenge))
s.enterState(LOGIN)
return
}
default:
{
s.send(fmt.Sprintf("500 Unsupported AUTH method: %v", authMethod))
return
}
}
} else if cmd == "MAIL" { } else if cmd == "MAIL" {
// Capture group 1: from address. 2: optional params. // Capture group 1: from address. 2: optional params.
m := fromRegex.FindStringSubmatch(arg) m := fromRegex.FindStringSubmatch(arg)

View File

@@ -108,6 +108,46 @@ func TestEmptyEnvelope(t *testing.T) {
} }
} }
// Test AUTH
func TestAuth(t *testing.T) {
ds := test.NewStore()
server, logbuf, teardown := setupSMTPServer(ds)
defer teardown()
//PLAIN AUTH
script := []scriptStep{
{"EHLO localhost", 250},
{"AUTH PLAIN aW5idWNrZXQ6cGFzc3dvcmQK", 235},
{"RSET", 250},
{"AUTH GSSAPI aW5idWNrZXQ6cGFzc3dvcmQK", 500},
{"RSET", 250},
{"AUTH PLAIN", 500},
{"RSET", 250},
{"AUTH PLAIN aW5idWNrZXQ6cG Fzc3dvcmQK", 500},
}
if err := playSession(t, server, script); err != nil {
t.Error(err)
}
//LOGIN AUTH
script = []scriptStep{
{"EHLO localhost", 250},
{"AUTH LOGIN", 334},
{"USERNAME", 334},
{"PASSWORD", 235},
}
if err := playSession(t, server, script); err != nil {
t.Error(err)
}
if t.Failed() {
// Wait for handler to finish logging
time.Sleep(2 * time.Second)
// Dump buffered log data if there was a failure
_, _ = io.Copy(os.Stderr, logbuf)
}
}
// Test commands in READY state // Test commands in READY state
func TestReadyState(t *testing.T) { func TestReadyState(t *testing.T) {
ds := test.NewStore() ds := test.NewStore()