mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
169 lines
5 KiB
Go
169 lines
5 KiB
Go
package ldap
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
ldap "github.com/vjeantet/ldapserver"
|
|
)
|
|
|
|
// New returns a new ldap server instance
|
|
func New(opts ...Option) *server {
|
|
x := new(server)
|
|
for _, o := range opts {
|
|
o(x)
|
|
}
|
|
x.Server = ldap.NewServer()
|
|
|
|
// The underlying server only permits a single handler and aborts
|
|
// the process on a second registration, so a stable delegating
|
|
// handler is registered once and SetDomain swaps the routed mux
|
|
// underneath it.
|
|
x.Handle(x)
|
|
|
|
x.buildRoutes()
|
|
|
|
return x
|
|
}
|
|
|
|
// buildRoutes (re)creates the route mux. It is called by New and again
|
|
// by SetDomain so that the naming-convention dependent routes always
|
|
// reflect the current domain without ever leaving duplicates behind.
|
|
func (s *server) buildRoutes() {
|
|
routes := ldap.NewRouteMux()
|
|
routes.NotFound(s.guard(s.handleNotFound, failOperationError))
|
|
routes.Abandon(s.guard(s.handleAbandon, nil))
|
|
routes.Bind(s.guard(s.handleBind, failBindError))
|
|
|
|
routes.Search(s.guard(s.handleSearchDSE, failSearchError)).
|
|
BaseDn("").
|
|
Scope(ldap.SearchRequestScopeBaseObject).
|
|
Filter("(objectclass=*)").
|
|
Label("Search - ROOT DSE")
|
|
|
|
if len(s.nc) > 0 {
|
|
base := strings.Join(s.nc, ",")
|
|
|
|
routes.Search(s.guard(s.handleSearchEntities, failSearchError)).
|
|
BaseDn("ou=entities," + base).
|
|
Scope(ldap.SearchRequestHomeSubtree).
|
|
Label("Search - Entities")
|
|
|
|
routes.Search(s.guard(s.handleSearchGroups, failSearchError)).
|
|
BaseDn("ou=groups," + base).
|
|
Scope(ldap.SearchRequestHomeSubtree).
|
|
Label("Search - Groups")
|
|
}
|
|
|
|
s.routes = routes
|
|
}
|
|
|
|
// Serve serves a plaintext DSA on the provided bind string. It
|
|
// returns once the listener is bound; the server keeps running in the
|
|
// background until Stop is called.
|
|
func (s *server) Serve(bind string) error {
|
|
return s.listenAndServe(bind)
|
|
}
|
|
|
|
// ServeTLS serves a TLS encrypted DSA on the provided bindstring
|
|
// using a key/cert pair located at the paths provided. Key and
|
|
// certificate should be PEM encoded.
|
|
func (s *server) ServeTLS(bind, keypath, certpath string) error {
|
|
cert, err := tls.LoadX509KeyPair(certpath, keypath)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating certificate chain: %s", err)
|
|
}
|
|
cfg := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
return s.listenAndServe(bind, func(srv *ldap.Server) {
|
|
srv.Listener = tls.NewListener(srv.Listener, cfg)
|
|
})
|
|
}
|
|
|
|
// listenAndServe binds the listening socket synchronously so that
|
|
// bind errors are reported to the caller, then serves in the
|
|
// background. The underlying server assigns its Listener field before
|
|
// invoking the option functions it is handed, so closing the started
|
|
// channel from an option guarantees that bookkeeping (and any TLS
|
|
// wrapping) is complete, and in a reliably ordered way, before Serve
|
|
// returns. Stop depends on reading that field.
|
|
func (s *server) listenAndServe(bind string, opts ...func(*ldap.Server)) error {
|
|
started := make(chan struct{})
|
|
failed := make(chan error, 1)
|
|
go func() {
|
|
opts := append(opts, func(*ldap.Server) { close(started) })
|
|
if err := s.Server.ListenAndServe(bind, opts...); err != nil {
|
|
select {
|
|
case <-started:
|
|
s.l.Error("Error from main server thread", "error", err)
|
|
default:
|
|
failed <- err
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case <-started:
|
|
return nil
|
|
case err := <-failed:
|
|
return fmt.Errorf("error creating listener: %s", err)
|
|
}
|
|
}
|
|
|
|
func (s *server) handleNotFound(w ldap.ResponseWriter, r *ldap.Message) {
|
|
res := ldap.NewResponse(ldap.LDAPResultUnwillingToPerform)
|
|
res.SetDiagnosticMessage("Operation not implemented by server")
|
|
w.Write(res)
|
|
}
|
|
|
|
func (s *server) handleAbandon(w ldap.ResponseWriter, m *ldap.Message) {
|
|
var req = m.GetAbandonRequest()
|
|
// retreive the request to abandon, and send a abort signal to it
|
|
if requestToAbandon, ok := m.Client.GetMessageByID(int(req)); ok {
|
|
requestToAbandon.Abandon()
|
|
}
|
|
}
|
|
|
|
// SetDomain configures the naming context from a plain domain name
|
|
// such as "example.com". NetAuth has no notion of a base DN, so it is
|
|
// supplied here: the domain is split on dots, each label becomes a
|
|
// dc component, and dc=netauth is prepended. It must be called after
|
|
// New and is safe to call more than once.
|
|
func (s *server) SetDomain(domain string) error {
|
|
if domain == "" {
|
|
return errors.New("a domain must be configured")
|
|
}
|
|
|
|
labels := strings.Split(domain, ".")
|
|
nc := []string{"dc=netauth"}
|
|
for _, l := range labels {
|
|
l = strings.TrimSpace(l)
|
|
if l == "" {
|
|
return errors.New("domain contains an empty label")
|
|
}
|
|
nc = append(nc, "dc="+l)
|
|
}
|
|
|
|
s.nc = nc
|
|
s.buildRoutes()
|
|
return nil
|
|
}
|
|
|
|
// ServeLDAP implements the underlying server's Handler interface. It
|
|
// is registered once with the server and forwards to whatever route
|
|
// mux is current, which lets SetDomain rebuild routing without
|
|
// re-registering a handler (the server aborts on a second
|
|
// registration).
|
|
func (s *server) ServeLDAP(w ldap.ResponseWriter, r *ldap.Message) {
|
|
if s.routes == nil {
|
|
res := ldap.NewResponse(ldap.LDAPResultUnavailable)
|
|
res.SetDiagnosticMessage("server is not configured with a domain")
|
|
w.Write(res)
|
|
return
|
|
}
|
|
s.routes.ServeLDAP(w, r)
|
|
}
|