1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +00:00
Files
go-chasquid-smtp/internal/dovecot/dovecot.go
Alberto Bertogli 51e7c5c19e dovecot: Dovecot authentication package
This patch adds a new package which implements two basic primitives for
authenticating against dovecot ("user exists", and "check password").

It is still experimental/work in progress.
2018-02-10 23:01:23 +00:00

277 lines
6.7 KiB
Go

// Package dovecot implements functions to interact with Dovecot's
// authentication service.
//
// In particular, it supports doing user authorization, and checking if a user
// exists. It is a very basic implementation, with only the minimum needed to
// cover chasquid's needs.
//
// https://wiki.dovecot.org/Design/AuthProtocol
// https://wiki.dovecot.org/Services#auth
package dovecot
import (
"encoding/base64"
"errors"
"fmt"
"net"
"net/textproto"
"os"
"strings"
"time"
"unicode"
)
// Default timeout to use. We expect Dovecot to be quite fast, but don't want
// to hang forever if something gets stuck.
const DefaultTimeout = 5 * time.Second
var (
ErrUsernameNotSafe = errors.New("username not safe (contains spaces)")
)
var defaultUserdbPaths = []string{
"/var/run/dovecot/auth-chasquid-userdb",
"/var/run/dovecot/auth-userdb",
}
var defaultClientPaths = []string{
"/var/run/dovecot/auth-chasquid-client",
"/var/run/dovecot/auth-client",
}
// Auth represents a particular Dovecot auth service to use.
type Auth struct {
userdbAddr string
clientAddr string
// Timeout for connection and I/O operations (applies on each call).
// Set to DefaultTimeout by NewAuth.
Timeout time.Duration
}
// NewAuth returns a new connection against Dovecot authentication service. It
// takes the addresses of userdb and client sockets (usually paths as
// configured in dovecot).
func NewAuth(userdb, client string) *Auth {
return &Auth{
userdbAddr: userdb,
clientAddr: client,
Timeout: DefaultTimeout,
}
}
func (a *Auth) String() string {
return fmt.Sprintf("DovecotAuth(%q, %q)", a.userdbAddr, a.clientAddr)
}
// Check to see if this auth is valid (but may not be working).
func (a *Auth) Check() error {
// We intentionally don't connect or complete any handshakes because
// dovecot may not be up yet, even thought it may be configured properly.
// Just check that the addresses are valid sockets.
if !isUnixSocket(a.userdbAddr) {
return fmt.Errorf("userdb is not an unix socket")
}
if !isUnixSocket(a.clientAddr) {
return fmt.Errorf("client is not an unix socket")
}
return nil
}
// Does user exist?
func (a *Auth) Exists(user string) (bool, error) {
if !isUsernameSafe(user) {
return false, ErrUsernameNotSafe
}
conn, err := a.dial("unix", a.userdbAddr)
if err != nil {
return false, err
}
defer conn.Close()
// Dovecot greets us with version and server pid.
// VERSION\t<major>\t<minor>
// SPID\t<pid>
err = expect(conn, "VERSION\t1")
if err != nil {
return false, fmt.Errorf("error receiving version: %v", err)
}
err = expect(conn, "SPID\t")
if err != nil {
return false, fmt.Errorf("error receiving SPID: %v", err)
}
// Send our version, and then the request.
err = write(conn, "VERSION\t1\t1\n")
if err != nil {
return false, err
}
err = write(conn, fmt.Sprintf("USER\t1\t%s\tservice=smtp\n", user))
if err != nil {
return false, err
}
// Get the response, and we're done.
resp, err := conn.ReadLine()
if err != nil {
return false, fmt.Errorf("error receiving response: %v", err)
} else if strings.HasPrefix(resp, "USER\t1\t"+user+"\t") {
return true, nil
} else if strings.HasPrefix(resp, "NOTFOUND\t") {
return false, nil
}
return false, fmt.Errorf("invalid response: %q", resp)
}
// Is the password valud for the user?
func (a *Auth) Authenticate(user, passwd string) (bool, error) {
if !isUsernameSafe(user) {
return false, ErrUsernameNotSafe
}
conn, err := a.dial("unix", a.clientAddr)
if err != nil {
return false, err
}
defer conn.Close()
// Send our version, and then our PID.
err = write(conn, fmt.Sprintf("VERSION\t1\t1\nCPID\t%d\n", os.Getpid()))
if err != nil {
return false, err
}
// Read the server-side handshake. We don't care about the contents
// really, so just read all lines until we see the DONE.
for {
resp, err := conn.ReadLine()
if err != nil {
return false, fmt.Errorf("error receiving handshake: %v", err)
}
if resp == "DONE" {
break
}
}
// We only support PLAIN authentication, so construct the request.
// Note we set the "secured" option, with the assumpition that we got the
// password via a secure channel (like TLS). This is always true for
// chasquid by design, and simplifies the API.
// TODO: does dovecot handle utf8 domains well? do we need to encode them
// in IDNA first?
resp := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s\x00%s\x00%s", user, user, passwd)))
err = write(conn, fmt.Sprintf(
"AUTH\t1\tPLAIN\tservice=smtp\tsecured\tno-penalty\tnologin\tresp=%s\n", resp))
if err != nil {
return false, err
}
// Get the response, and we're done.
resp, err = conn.ReadLine()
if err != nil {
return false, fmt.Errorf("error receiving response: %v", err)
} else if strings.HasPrefix(resp, "OK\t1") {
return true, nil
} else if strings.HasPrefix(resp, "FAIL\t1") {
return false, nil
}
return false, fmt.Errorf("invalid response: %q", resp)
}
func (a *Auth) Reload() error {
return nil
}
func (a *Auth) dial(network, addr string) (*textproto.Conn, error) {
nc, err := net.DialTimeout(network, addr, a.Timeout)
if err != nil {
return nil, err
}
nc.SetDeadline(time.Now().Add(a.Timeout))
return textproto.NewConn(nc), nil
}
func expect(conn *textproto.Conn, prefix string) error {
resp, err := conn.ReadLine()
if err != nil {
return err
}
if !strings.HasPrefix(resp, prefix) {
return fmt.Errorf("got %q", resp)
}
return nil
}
func write(conn *textproto.Conn, msg string) error {
_, err := fmt.Fprintf(conn.W, msg)
if err != nil {
return err
}
return conn.W.Flush()
}
// isUsernameSafe to use in the dovecot protocol?
// Unfotunately dovecot's protocol is not very robust wrt. whitespace,
// so we need to be careful.
func isUsernameSafe(user string) bool {
for _, r := range user {
if unicode.IsSpace(r) {
return false
}
}
return true
}
// Autodetect where the dovecot authentication paths are, and return an Auth
// instance for them. If any of userdb or client are != "", they will be used
// and not autodetected.
func Autodetect(userdb, client string) *Auth {
// If both are given, no need to autodtect.
if userdb != "" && client != "" {
return NewAuth(userdb, client)
}
var userdbs, clients []string
if userdb != "" {
userdbs = append(userdbs, userdb)
}
if client != "" {
clients = append(clients, client)
}
if len(userdbs) == 0 {
userdbs = append(userdbs, defaultUserdbPaths...)
}
if len(clients) == 0 {
clients = append(clients, defaultClientPaths...)
}
// Go through each possiblity, return the first auth that works.
for _, u := range userdbs {
for _, c := range clients {
a := NewAuth(u, c)
if a.Check() == nil {
return a
}
}
}
return nil
}
func isUnixSocket(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
return fi.Mode()&os.ModeSocket != 0
}