1package ldap23import (4 "context"5 "crypto/tls"6 "fmt"7 "net"8 "net/url"9 "strings"10 "sync"11 "time"1213 "github.com/foxcpp/maddy/framework/config"14 tls2 "github.com/foxcpp/maddy/framework/config/tls"15 "github.com/foxcpp/maddy/framework/log"16 "github.com/foxcpp/maddy/framework/module"17 "github.com/go-ldap/ldap/v3"18)1920const modName = "auth.ldap"2122type Auth struct {23 instName string2425 urls []string26 readBind func(*ldap.Conn) error27 startls bool28 tlsCfg tls.Config29 dialer *net.Dialer30 requestTimeout time.Duration3132 dnTemplate string33 // or34 baseDN string35 filterTemplate string3637 conn *ldap.Conn38 connLock sync.Mutex3940 log log.Logger41}4243func New(modName, instName string, _, inlineArgs []string) (module.Module, error) {44 return &Auth{45 instName: instName,46 log: log.Logger{Name: modName},47 urls: inlineArgs,48 }, nil49}5051func (a *Auth) Init(cfg *config.Map) error {52 a.dialer = &net.Dialer{}5354 cfg.Bool("debug", true, false, &a.log.Debug)55 cfg.Custom("tls_client", true, false, func() (interface{}, error) {56 return tls.Config{}, nil57 }, tls2.TLSClientBlock, &a.tlsCfg)58 cfg.Callback("urls", func(m *config.Map, node config.Node) error {59 a.urls = append(a.urls, node.Args...)60 return nil61 })62 cfg.Custom("bind", false, false, func() (interface{}, error) {63 return func(*ldap.Conn) error {64 return nil65 }, nil66 }, readBindDirective, &a.readBind)67 cfg.Bool("starttls", false, false, &a.startls)68 cfg.Duration("connect_timeout", false, false, time.Minute, &a.dialer.Timeout)69 cfg.Duration("request_timeout", false, false, time.Minute, &a.requestTimeout)70 cfg.String("dn_template", false, false, "", &a.dnTemplate)71 cfg.String("base_dn", false, false, "", &a.baseDN)72 cfg.String("filter", false, false, "", &a.filterTemplate)73 if _, err := cfg.Process(); err != nil {74 return err75 }7677 if a.dnTemplate == "" {78 if a.baseDN == "" {79 return fmt.Errorf("auth.ldap: base_dn not set")80 }81 if a.filterTemplate == "" {82 return fmt.Errorf("auth.ldap: filter not set")83 }84 } else {85 if a.baseDN != "" || a.filterTemplate != "" {86 return fmt.Errorf("auth.ldap: search directives set when dn_template is used")87 }88 }8990 if module.NoRun {91 return nil92 }9394 var err error95 a.conn, err = a.newConn()96 if err != nil {97 return fmt.Errorf("auth.ldap: %w", err)98 }99 return nil100}101102func readBindDirective(c *config.Map, n config.Node) (interface{}, error) {103 if len(n.Args) == 0 {104 return nil, fmt.Errorf("auth.ldap: auth expects at least one argument")105 }106 switch n.Args[0] {107 case "off":108 return func(*ldap.Conn) error { return nil }, nil109 case "unauth":110 if len(n.Args) == 2 {111 return func(c *ldap.Conn) error {112 return c.UnauthenticatedBind(n.Args[1])113 }, nil114 }115 return func(c *ldap.Conn) error {116 return c.UnauthenticatedBind("")117 }, nil118 case "plain":119 if len(n.Args) != 3 {120 return nil, fmt.Errorf("auth.ldap: username and password expected for plaintext bind")121 }122 return func(c *ldap.Conn) error {123 return c.Bind(n.Args[1], n.Args[2])124 }, nil125 case "external":126 return (*ldap.Conn).ExternalBind, nil127 }128 return nil, fmt.Errorf("auth.ldap: unknown bind authentication: %v", n.Args[0])129}130131func (a *Auth) Name() string {132 return modName133}134135func (a *Auth) InstanceName() string {136 return a.instName137}138139func (a *Auth) newConn() (*ldap.Conn, error) {140 var (141 conn *ldap.Conn142 tlsCfg *tls.Config143 )144 for _, u := range a.urls {145 parsedURL, err := url.Parse(u)146 if err != nil {147 return nil, fmt.Errorf("auth.ldap: invalid server URL: %w", err)148 }149 hostname := parsedURL.Host150 a.tlsCfg.ServerName = strings.Split(hostname, ":")[0]151 tlsCfg = a.tlsCfg.Clone()152153 conn, err = ldap.DialURL(u, ldap.DialWithDialer(a.dialer), ldap.DialWithTLSConfig(tlsCfg))154 if err != nil {155 a.log.Error("cannot contact directory server", err, "url", u)156 continue157 }158 break159 }160 if conn == nil {161 return nil, fmt.Errorf("auth.ldap: all directory servers are unreachable")162 }163164 if a.requestTimeout != 0 {165 conn.SetTimeout(a.requestTimeout)166 }167168 if a.startls {169 if err := conn.StartTLS(tlsCfg); err != nil {170 return nil, fmt.Errorf("auth.ldap: %w", err)171 }172 }173174 if err := a.readBind(conn); err != nil {175 return nil, fmt.Errorf("auth.ldap: %w", err)176 }177178 return conn, nil179}180181func (a *Auth) getConn() (*ldap.Conn, error) {182 a.connLock.Lock()183 if a.conn == nil {184 conn, err := a.newConn()185 if err != nil {186 a.connLock.Unlock()187 return nil, err188 }189 a.conn = conn190 }191 if a.conn.IsClosing() {192 a.conn.Close()193 conn, err := a.newConn()194 if err != nil {195 a.connLock.Unlock()196 return nil, err197 }198 a.conn = conn199 }200 return a.conn, nil201}202203func (a *Auth) returnConn(conn *ldap.Conn) {204 defer a.connLock.Unlock()205 if err := a.readBind(conn); err != nil {206 a.log.Error("failed to rebind for reading", err)207 conn.Close()208 a.conn = nil209 }210 if a.conn != conn {211 a.conn.Close()212 }213 a.conn = conn214}215216func (a *Auth) Lookup(_ context.Context, username string) (string, bool, error) {217 conn, err := a.getConn()218 if err != nil {219 return "", false, err220 }221 defer a.returnConn(conn)222223 var userDN string224 if a.dnTemplate != "" {225 return "", false, fmt.Errorf("auth.ldap: lookups require search config but dn_template is used")226 } else {227 req := ldap.NewSearchRequest(228 a.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,229 2, 0, false,230 strings.ReplaceAll(a.filterTemplate, "{username}", username),231 []string{"dn"}, nil)232 res, err := conn.Search(req)233 if err != nil {234 return "", false, fmt.Errorf("auth.ldap: search: %w", err)235 }236 if len(res.Entries) > 1 {237 return "", false, fmt.Errorf("auth.ldap: too manu entries returned (%d)", len(res.Entries))238 }239 if len(res.Entries) == 0 {240 return "", false, nil241 }242 userDN = res.Entries[0].DN243 }244245 return userDN, true, nil246}247248func (a *Auth) AuthPlain(username, password string) error {249 conn, err := a.getConn()250 if err != nil {251 return err252 }253 defer a.returnConn(conn)254255 var userDN string256 if a.dnTemplate != "" {257 userDN = strings.ReplaceAll(a.dnTemplate, "{username}", username)258 } else {259 req := ldap.NewSearchRequest(260 a.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,261 2, 0, false,262 strings.ReplaceAll(a.filterTemplate, "{username}", username),263 []string{"dn"}, nil)264 res, err := conn.Search(req)265 if err != nil {266 return fmt.Errorf("auth.ldap: search: %w", err)267 }268 if len(res.Entries) > 1 {269 return fmt.Errorf("auth.ldap: too manu entries returned (%d)", len(res.Entries))270 }271 if len(res.Entries) == 0 {272 return module.ErrUnknownCredentials273 }274 userDN = res.Entries[0].DN275 }276277 if err := conn.Bind(userDN, password); err != nil {278 return module.ErrUnknownCredentials279 }280281 return nil282}283284func init() {285 var _ module.PlainAuth = &Auth{}286 var _ module.Table = &Auth{}287 module.Register(modName, New)288 module.Register("table.ldap", New)289}