mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
committed by
James Hillyerd
parent
3c5960aba0
commit
c096f018d6
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user