Files
ldaps/server.go
2026-08-25 14:48:54 -07:00

636 lines
18 KiB
Go

package ldaps
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"runtime/debug"
"strings"
"sync"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
const (
LDAPBindAuthSimple = 0
LDAPBindAuthSASL = 3
oidStartTLS = "1.3.6.1.4.1.1466.20037"
)
var (
ErrEmpty = errors.New("")
ErrInternal = errors.New("internal error")
ErrNotImplemented = errors.New("not implemented")
)
type Binder interface {
Bind(ctx context.Context, bindDN, bindSimplePw string, conn net.Conn) (*ldap.SimpleBindResult, error)
}
type Searcher interface {
Search(ctx context.Context, boundDN string, req ldap.SearchRequest, conn net.Conn) (*ldap.SearchResult, error)
}
type Extender interface {
Extended(ctx context.Context, boundDN string, req ldap.ExtendedRequest, conn net.Conn) error
}
type Abandoner interface {
Abandon(ctx context.Context, boundDN string, conn net.Conn) error
}
type Adder interface {
Add(ctx context.Context, boundDN string, req ldap.AddRequest, conn net.Conn) error
}
type Modifier interface {
Modify(ctx context.Context, boundDN string, req ldap.ModifyRequest, conn net.Conn) (*ldap.ModifyResult, error)
}
type Deleter interface {
Delete(ctx context.Context, boundDN, deleteDN string, conn net.Conn) error
}
type ModifyDNr interface {
ModifyDN(ctx context.Context, boundDN string, req ldap.ModifyDNRequest, conn net.Conn) error
}
type Comparer interface {
Compare(ctx context.Context, boundDN string, req ldap.CompareRequest, conn net.Conn) error
}
type Closer interface {
Close(ctx context.Context, boundDN string, conn net.Conn)
}
type Server struct {
BindFns map[string]Binder
SearchFns map[string]Searcher
AddFns map[string]Adder
ModifyFns map[string]Modifier
DeleteFns map[string]Deleter
ModifyDNFns map[string]ModifyDNr
CompareFns map[string]Comparer
AbandonFns map[string]Abandoner
ExtendedFns map[string]Extender
CloseFns map[string]Closer
Context context.Context
cancel func()
EnforceLDAP bool
stats *stats
ln net.Listener
// If set, server will accept StartTLS.
TLSConfig *tls.Config
}
type Stats struct {
Conns int
Binds int
Unbinds int
Searches int
NotImplemented int
}
type stats struct {
Stats
statsMutex sync.Mutex
}
func NewServer() *Server {
return NewServerContext(context.Background())
}
func NewServerContext(ctx context.Context) *Server {
s := new(Server)
s.Context, s.cancel = context.WithCancel(ctx)
s.BindFns = make(map[string]Binder)
s.SearchFns = make(map[string]Searcher)
s.ExtendedFns = make(map[string]Extender)
s.AbandonFns = make(map[string]Abandoner)
s.AddFns = make(map[string]Adder)
s.ModifyFns = make(map[string]Modifier)
s.DeleteFns = make(map[string]Deleter)
s.ModifyDNFns = make(map[string]ModifyDNr)
s.CompareFns = make(map[string]Comparer)
s.CloseFns = make(map[string]Closer)
d := defaultHandler{}
s.BindFunc("", d)
s.SearchFunc("", d)
s.ExtendedFunc("", d)
s.AbandonFunc("", d)
s.AddFunc("", d)
s.ModifyFunc("", d)
s.DeleteFunc("", d)
s.ModifyDNFunc("", d)
s.CompareFunc("", d)
s.CloseFunc("", d)
s.stats = nil
return s
}
func (server *Server) BindFunc(baseDN string, f Binder) {
server.BindFns[baseDN] = f
}
func (server *Server) SearchFunc(baseDN string, f Searcher) {
server.SearchFns[baseDN] = f
}
func (server *Server) AddFunc(baseDN string, f Adder) {
server.AddFns[baseDN] = f
}
func (server *Server) ModifyFunc(baseDN string, f Modifier) {
server.ModifyFns[baseDN] = f
}
func (server *Server) DeleteFunc(baseDN string, f Deleter) {
server.DeleteFns[baseDN] = f
}
func (server *Server) ModifyDNFunc(baseDN string, f ModifyDNr) {
server.ModifyDNFns[baseDN] = f
}
func (server *Server) CompareFunc(baseDN string, f Comparer) {
server.CompareFns[baseDN] = f
}
func (server *Server) AbandonFunc(baseDN string, f Abandoner) {
server.AbandonFns[baseDN] = f
}
func (server *Server) ExtendedFunc(baseDN string, f Extender) {
server.ExtendedFns[baseDN] = f
}
func (server *Server) CloseFunc(baseDN string, f Closer) {
server.CloseFns[baseDN] = f
}
func (server *Server) SetStats(enable bool) {
if enable {
server.stats = &stats{}
} else {
server.stats = nil
}
}
func (server *Server) GetStats() Stats {
defer server.stats.statsMutex.Unlock()
server.stats.statsMutex.Lock()
return server.stats.Stats
}
func (server *Server) ListenAndServe(listenString string) error {
ln, err := net.Listen("tcp", listenString)
if err != nil {
return err
}
return server.Serve(ln)
}
func (server *Server) Serve(ln net.Listener) error {
server.ln = ln
newConn := make(chan net.Conn)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
if !errors.Is(err, net.ErrClosed) {
log.Printf("Error accepting network connection: %v", err)
}
break
}
newConn <- conn
}
}()
listener:
for {
select {
case c := <-newConn:
server.stats.countConns(1)
go server.handleConnection(c)
case <-server.Context.Done():
server.cancel = nil
server.Close()
break listener
}
}
return nil
}
// Close closes the underlying net.Listener, and waits for confirmation
func (server *Server) Close() {
if server.ln != nil {
server.ln.Close()
server.ln = nil
}
if server.cancel != nil {
server.cancel()
}
}
func handleCloseFunc(ctx context.Context, fnName string, closeFn Closer, boundDN string, conn net.Conn) {
defer func() {
panicResult := recover()
if panicResult != nil {
log.Printf("Recovered from panic in handleClose(%s): %s\n%s", fnName, panicResult, string(debug.Stack()))
}
}()
closeFn.Close(ctx,boundDN, conn)
}
func handleClose(ctx context.Context, fns map[string]Closer, boundDN string, conn net.Conn) {
for fnName, closeFn := range fns {
handleCloseFunc(ctx, fnName, closeFn, boundDN, conn)
}
}
func (server *Server) handleConnection(conn net.Conn) {
ctx, cancel := context.WithCancel(server.Context)
boundDN := "" // "" == anonymous
defer func() {
panicResult := recover()
if panicResult != nil {
log.Printf("Recovered from panic in handleConnection: %s\n%s", panicResult, string(debug.Stack()))
}
handleClose(ctx, server.CloseFns, boundDN, conn)
conn.Close()
cancel()
}()
handler:
for {
// read incoming LDAP packet
packet, err := ber.ReadPacket(conn)
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { // Client closed connection
break
} else if err != nil {
log.Printf("handleConnection ber.ReadPacket ERROR: %v", err)
break
}
// sanity check this packet
if len(packet.Children) < 2 {
log.Print("len(packet.Children) < 2")
break
}
// check the message ID and ClassType
messageID64, ok := packet.Children[0].Value.(int64)
if !ok {
log.Print("malformed messageID")
break
}
messageID := uint64(messageID64)
req := packet.Children[1]
if req.ClassType != ber.ClassApplication {
log.Print("req.ClassType != ber.ClassApplication")
break
}
// handle controls if present
controls := []ldap.Control{}
if len(packet.Children) > 2 {
for _, child := range packet.Children[2].Children {
control, err := ldap.DecodeControl(child)
if err != nil {
log.Printf("DecodeControl error %v", err)
responsePacket := encodeProtocolErrorResponse(messageID, req.Tag, "", nil)
if responsePacket != nil {
if err = sendPacket(conn, responsePacket); err != nil {
log.Printf("sendPacket error %v", err)
}
}
break handler
}
controls = append(controls, control)
}
}
// dispatch the LDAP operation
switch req.Tag { // ldap op code
default:
name, ok := ldap.ApplicationMap[uint8(req.Tag)]
if !ok {
name = "Unknown"
}
log.Printf("Unhandled operation: %s [%d]", name, req.Tag)
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationAddResponse, ldap.LDAPResultUnavailable, fmt.Sprintf("Unhandled operation: %s [%d]", name, req.Tag), nil)
if err = sendPacket(conn, responsePacket); err != nil {
log.Printf("sendPacket error %s", err.Error())
}
break handler
case ldap.ApplicationBindRequest:
server.stats.countBinds(1)
dn, _, err := HandleBindRequest(ctx, req, server.BindFns, conn) // TODO: Handle SimpleBindResult
resultCode, message := StatusResult(err)
if resultCode == ldap.LDAPResultSuccess {
boundDN = dn
} else {
log.Printf("Error Binding: %s", err)
}
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationBindResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
boundDN = ""
break handler
}
case ldap.ApplicationSearchRequest:
server.stats.countSearches(1)
err = HandleSearchRequest(ctx, req, &controls, messageID, boundDN, server, conn)
resultCode, message := StatusResult(err)
if resultCode != ldap.LDAPResultSuccess {
log.Printf("Error Searching: %s", err)
}
supportedControls := false
for _, control := range controls {
if control.GetControlType() == ldap.ControlTypePaging {
supportedControls = true
break
}
}
if !(supportedControls && resultCode == ldap.LDAPResultSuccess) {
controls = nil
}
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationSearchResultDone, resultCode, message, controls)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
case ldap.ApplicationUnbindRequest:
server.stats.countUnbinds(1)
break handler // simply disconnect
case ldap.ApplicationExtendedRequest:
var tlsConn net.Conn
if len(req.Children) > 0 && ber.DecodeString(req.Children[0].Data.Bytes()) == oidStartTLS {
if server.TLSConfig != nil {
tlsConn = tls.Server(conn, server.TLSConfig)
err = nil
} else {
err = ldap.NewError(ldap.LDAPResultUnavailable, errors.New("TLS not configured"))
}
} else {
// Wasn't an upgrade. Pass through.
err = HandleExtendedRequest(ctx, req, boundDN, server.ExtendedFns, conn)
}
resultCode, message := StatusResult(err)
if resultCode != ldap.LDAPResultSuccess {
log.Printf("Error with extended request: %s", err)
}
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationExtendedResponse, resultCode, message, nil)
if err = sendPacket(conn, responsePacket); err != nil {
log.Printf("sendPacket error %v", err)
break handler
}
if tlsConn != nil {
conn = tlsConn
}
case ldap.ApplicationAbandonRequest:
err = HandleAbandonRequest(ctx, req, boundDN, server.AbandonFns, conn)
// We don't need to do any extra handling here abandon does not expect a response
if err != nil {
log.Printf("Error Abandoning Request: %s", err)
break handler
}
break handler
case ldap.ApplicationAddRequest:
err = HandleAddRequest(ctx, req, boundDN, server.AddFns, conn)
resultCode, message := StatusResult(err)
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationAddResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
case ldap.ApplicationModifyRequest:
_, err = HandleModifyRequest(ctx, req, boundDN, server.ModifyFns, conn) // TODO: Handle ModifyResult
resultCode, message := StatusResult(err)
if resultCode != ldap.LDAPResultSuccess {
log.Printf("Error Modifying: %s", err)
}
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationModifyResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
case ldap.ApplicationDelRequest:
err = HandleDeleteRequest(ctx, req, boundDN, server.DeleteFns, conn)
resultCode, message := StatusResult(err)
if resultCode != ldap.LDAPResultSuccess {
log.Printf("Error Deleting: %s", err)
}
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationDelResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
case ldap.ApplicationModifyDNRequest:
err = HandleModifyDNRequest(ctx, req, boundDN, server.ModifyDNFns, conn)
resultCode, message := StatusResult(err)
if resultCode != ldap.LDAPResultSuccess && resultCode != ldap.LDAPResultCompareFalse && resultCode != ldap.LDAPResultCompareTrue {
log.Printf("Error Modifying DN: %s", err)
}
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationModifyDNResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
case ldap.ApplicationCompareRequest:
err = HandleCompareRequest(ctx, req, boundDN, server.CompareFns, conn)
resultCode, message := StatusResult(err)
if err = sendPacket(conn, encodeLDAPResponse(messageID, ldap.ApplicationCompareResponse, resultCode, message, nil)); err != nil {
log.Printf("sendPacket error: %s", err.Error())
break handler
}
}
}
}
func sendPacket(conn net.Conn, packet *ber.Packet) error {
_, err := conn.Write(packet.Bytes())
if err != nil {
log.Printf("Error Sending Message: %v", err)
return err
}
return nil
}
func encodeProtocolErrorResponse(messageID uint64, requestType ber.Tag, message string, controls []ldap.Control) *ber.Packet {
switch requestType {
case ldap.ApplicationSearchRequest:
return encodeLDAPResponse(messageID, ldap.ApplicationSearchResultDone, ldap.LDAPResultProtocolError, message, controls)
case ldap.ApplicationBindRequest, ldap.ApplicationModifyRequest, ldap.ApplicationAddRequest, ldap.ApplicationDelRequest, ldap.ApplicationModifyDNRequest, ldap.ApplicationCompareRequest, ldap.ApplicationExtendedRequest:
return encodeLDAPResponse(messageID, uint8(requestType+1), ldap.LDAPResultProtocolError, message, controls)
default:
return nil
}
}
func routeFunc(dn string, funcNames []string) string {
bestPick := ""
bestPickWeight := 0
dnMatch := "," + strings.ToLower(dn)
var weight int
for _, fn := range funcNames {
if strings.HasSuffix(dnMatch, ","+fn) {
// empty string as 0, no-comma string 1 , etc
if fn == "" {
weight = 0
} else {
weight = strings.Count(fn, ",") + 1
}
if weight > bestPickWeight {
bestPick = fn
bestPickWeight = weight
}
}
}
return bestPick
}
func StatusResult(err error) (uint16, string) {
var (
code uint16 = ldap.LDAPResultSuccess
message string
)
if err == nil {
return code, message
}
e := &ldap.Error{}
if !errors.As(err, &e) {
e = &ldap.Error{ResultCode: ldap.LDAPResultOther, Err: ErrInternal}
}
// we can end up with e == nil if we pass the output of ApplyFilter which returns *ldap.ErrorType because errors.As will match on type
if e == nil {
return code, message
}
code = e.ResultCode
if e.Err != nil {
// TODO: use err.Error instead
message = e.Err.Error()
}
return code, message
}
func StatusCode(err error) uint16 {
code, _ := StatusResult(err)
return code
}
func encodeLDAPResponse(messageID uint64, responseType uint8, ldapResultCode uint16, message string, controls []ldap.Control) *ber.Packet {
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
response := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(responseType), nil, ldap.ApplicationMap[responseType])
response.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, message, "errorMessage: "))
responsePacket.AppendChild(response)
if len(controls) > 0 {
controlPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls")
for _, control := range controls {
controlPacket.AppendChild(control.Encode())
}
responsePacket.AppendChild(controlPacket)
}
return responsePacket
}
type defaultHandler struct{}
func (h defaultHandler) Bind(ctx context.Context, bindDN, bindSimplePw string, conn net.Conn) (*ldap.SimpleBindResult, error) {
return nil, ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Search(ctx context.Context, boundDN string, req ldap.SearchRequest, conn net.Conn) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Modify(ctx context.Context, boundDN string, req ldap.ModifyRequest, conn net.Conn) (*ldap.ModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Add(ctx context.Context, boundDN string, req ldap.AddRequest, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Delete(ctx context.Context, boundDN, deleteDN string, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) ModifyDN(ctx context.Context, boundDN string, req ldap.ModifyDNRequest, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Compare(ctx context.Context, boundDN string, req ldap.CompareRequest, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Abandon(ctx context.Context, boundDN string, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Extended(ctx context.Context, boundDN string, req ldap.ExtendedRequest, conn net.Conn) error {
return ldap.NewError(ldap.LDAPResultUnavailable, ErrNotImplemented)
}
func (h defaultHandler) Close(ctx context.Context, boundDN string, conn net.Conn) {} // conn will be closed automatically
func (stats *stats) countConns(delta int) {
if stats != nil {
stats.statsMutex.Lock()
stats.Conns += delta
stats.statsMutex.Unlock()
}
}
func (stats *stats) countBinds(delta int) {
if stats != nil {
stats.statsMutex.Lock()
stats.Binds += delta
stats.statsMutex.Unlock()
}
}
func (stats *stats) countUnbinds(delta int) {
if stats != nil {
stats.statsMutex.Lock()
stats.Unbinds += delta
stats.statsMutex.Unlock()
}
}
func (stats *stats) countSearches(delta int) {
if stats != nil {
stats.statsMutex.Lock()
stats.Searches += delta
stats.statsMutex.Unlock()
}
}
func (stats *stats) countNotImplemented(delta int) {
if stats != nil {
stats.statsMutex.Lock()
stats.NotImplemented += delta
stats.statsMutex.Unlock()
}
}