author | Alberto Bertogli
<albertito@blitiri.com.ar> 2021-06-11 19:05:41 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2021-06-11 19:09:15 UTC |
parent | 099e2e2269d8bc7b315dce596b4b92fdf46555f2 |
docs/dovecot.md | +3 | -2 |
internal/auth/auth.go | +21 | -14 |
internal/auth/auth_test.go | +2 | -1 |
test/t-11-dovecot/config/dovecot.conf.in | +3 | -2 |
test/t-11-dovecot/config/passwd | +2 | -1 |
test/t-11-dovecot/msmtprc | +5 | -0 |
test/t-11-dovecot/run.sh | +12 | -1 |
diff --git a/docs/dovecot.md b/docs/dovecot.md index a40cd77..853e7cc 100644 --- a/docs/dovecot.md +++ b/docs/dovecot.md @@ -31,8 +31,9 @@ accordingly. This lets chasquid issue authentication requests to dovecot. -Authentication requests sent by chasquid to dovecot will use the -fully-qualified user form, `user@domain`. +Authentication requests sent by chasquid to dovecot will pass on the username +as specified by the client. This will usually be either `user@domain`, or just +`user`. ## Configuring chasquid diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1a76e8c..4f02fdf 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -42,7 +42,7 @@ type Authenticator struct { // Fallback backend, to use when backends[domain] (which may not exist) // did not yield a positive result. // Note that this backend gets the user with the domain included, of the - // form "user@domain". + // form "user@domain" (if available). Fallback Backend // How long Authenticate calls should last, approximately. @@ -90,7 +90,11 @@ func (a *Authenticator) Authenticate(user, domain, password string) (bool, error } if a.Fallback != nil { - ok, err := a.Fallback.Authenticate(user+"@"+domain, password) + id := user + if domain != "" { + id = user + "@" + domain + } + ok, err := a.Fallback.Authenticate(id, password) tr.Debugf("Fallback: %v %v", ok, err) return ok, err } @@ -113,7 +117,11 @@ func (a *Authenticator) Exists(user, domain string) (bool, error) { } if a.Fallback != nil { - ok, err := a.Fallback.Exists(user + "@" + domain) + id := user + if domain != "" { + id = user + "@" + domain + } + ok, err := a.Fallback.Exists(id) tr.Debugf("Fallback: %v %v", ok, err) return ok, err } @@ -158,9 +166,11 @@ func (a *Authenticator) Reload() error { // // https://tools.ietf.org/html/rfc4954#section-4.1. // -// Either both ID match, or one of them is empty. -// We expect the ID to be "user@domain", which is NOT an RFC requirement but -// our own. +// Either both IDs match, or one of them is empty. +// +// We split the id into user@domain, since in most cases we expect that to be +// the used form, and normalize them. If there is no domain, we just return +// "" for it. The rest of the stack will know how to handle it. func DecodeResponse(response string) (user, domain, passwd string, err error) { buf, err := base64.StdEncoding.DecodeString(response) if err != nil { @@ -201,17 +211,14 @@ func DecodeResponse(response string) (user, domain, passwd string, err error) { return } - // Identity must be in the form "user@domain". - // This is NOT an RFC requirement, it's our own. + // Split identity into "user@domain", if possible. + user = identity idsp := strings.SplitN(identity, "@", 2) - if len(idsp) != 2 { - err = fmt.Errorf("identity must be in the form user@domain") - return + if len(idsp) >= 2 { + user = idsp[0] + domain = idsp[1] } - user = idsp[0] - domain = idsp[1] - // Normalize the user and domain. This is so users can write the username // in their own style and still can log in. For the domain, we use IDNA // and relevant transformations to turn it to utf8 which is what we use diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 0a1a124..ce1b840 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -19,6 +19,7 @@ func TestDecodeResponse(t *testing.T) { {"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass {"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass {"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff + {"dQB1AHBhc3M=", "u", "", "pass"}, // u\0u\0pass // "ñaca@ñeque\0\0clavaré" {"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"}, @@ -42,7 +43,7 @@ func TestDecodeResponse(t *testing.T) { failedCases := []string{ "", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00", - "a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass", "a\x00a\x00pass", + "a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass", "\xffa@b\x00\xffa@b\x00pass", } for _, c := range failedCases { diff --git a/test/t-11-dovecot/config/dovecot.conf.in b/test/t-11-dovecot/config/dovecot.conf.in index 77fbca3..9f9d962 100644 --- a/test/t-11-dovecot/config/dovecot.conf.in +++ b/test/t-11-dovecot/config/dovecot.conf.in @@ -5,10 +5,11 @@ ssl = no default_internal_user = $USER default_login_user = $USER -# Before auth checks, rename "u@d" to "u-AT-d". This exercises that chasquid +# Before auth checks, rename "u@d" to "u-x". This exercises that chasquid # handles well the case where the returned user information does not match the # requested user. -auth_username_format = "%n-AT-%d" +# We drop the domain, to exercise "naked" auth handling. +auth_username_format = "%n-x" passdb { driver = passwd-file diff --git a/test/t-11-dovecot/config/passwd b/test/t-11-dovecot/config/passwd index 2ceb0ee..61777ba 100644 --- a/test/t-11-dovecot/config/passwd +++ b/test/t-11-dovecot/config/passwd @@ -1 +1,2 @@ -user-AT-srv:{plain}password:1000:1000::/home/user +user-x:{plain}password:1000:1000::/home/user +naked-x:{plain}gun:1001:1001::/home/naked diff --git a/test/t-11-dovecot/msmtprc b/test/t-11-dovecot/msmtprc index 99c540d..54b7338 100644 --- a/test/t-11-dovecot/msmtprc +++ b/test/t-11-dovecot/msmtprc @@ -26,3 +26,8 @@ password secretpassword account badpasswd : default user user@srv password badsecretpassword + +account naked : default +from naked@srv +user naked +password gun diff --git a/test/t-11-dovecot/run.sh b/test/t-11-dovecot/run.sh index e93e0f2..0be1339 100755 --- a/test/t-11-dovecot/run.sh +++ b/test/t-11-dovecot/run.sh @@ -51,11 +51,22 @@ mkdir -p .logs chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config & wait_until_ready 1025 -# Send an email as user@srv successfully. +# Send an email as "user@srv" successfully. run_msmtp user@srv < content wait_for_file .mail/user@srv mail_diff content .mail/user@srv +# Send an email as "naked" successfully. +rm .mail/user@srv +run_msmtp -a naked user@srv < content +wait_for_file .mail/user@srv +mail_diff content .mail/user@srv + +# Send an email to the "naked" user successfully. +run_msmtp naked@srv < content +wait_for_file .mail/naked@srv +mail_diff content .mail/naked@srv + # Fail to send to nobody@srv (user does not exist). if run_msmtp nobody@srv < content 2> /dev/null; then fail "successfuly sent an email to a non-existent user"