Files
ldaps/server_search.go
timmy ab779df95a Allow sending an error message with the ldap response
Cleanup interfaces and allow the errorMessage ldap field to be used

Use the correct ldap server-side error code
LDAPResultNotSupported(92) is for a client side error
LDAPResultUnavailable(52) is for when a subsystem is not available
LDAPResultOther(80) is for generic server errors

Provide better errors

Fix binding dn to connection

Fix tests

Make handleSearchRequest conform to rfc4511

Plumb contexts through all connections

Add a server context instead of a public quit channel
2026-08-06 16:02:23 -07:00

235 lines
7.8 KiB
Go

package ldaps
import (
"context"
"errors"
"fmt"
"log"
"net"
"runtime/debug"
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleSearchRequest(ctx context.Context, req *ber.Packet, controls *[]ldap.Control, messageID uint64, boundDN string, server *Server, conn net.Conn) (resultErr error) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in SearchFn: %s\n%s", r, string(debug.Stack()))
resultErr = ldap.NewError(ldap.LDAPResultOther, fmt.Errorf("Search function panic: %s", r))
}
}()
searchReq, err := parseSearchRequest(req, controls)
if err != nil {
return err
}
filterPacket, err := ldap.CompileFilter(searchReq.Filter)
if err != nil {
return ldap.NewError(ldap.LDAPResultFilterError, fmt.Errorf("failed to compile filter: %q %w", searchReq.Filter, err))
}
fnNames := []string{}
for k := range server.SearchFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(searchReq.BaseDN, fnNames)
searchResp, err := server.SearchFns[fn].Search(ctx, boundDN, searchReq, conn)
if err != nil {
return err
}
if searchResp == nil {
searchResp = &ldap.SearchResult{}
}
if server.EnforceLDAP {
if searchReq.DerefAliases != ldap.NeverDerefAliases { // [-a {never|always|search|find}
// TODO: Server DerefAliases not supported: RFC4511 4.5.1.3
server.stats.countNotImplemented(1)
}
if searchReq.TimeLimit > 0 {
// TODO: Server TimeLimit not implemented
server.stats.countNotImplemented(1)
}
}
i := 0
searchReqBaseDNLower := strings.ToLower(searchReq.BaseDN)
for _, entry := range searchResp.Entries {
if server.EnforceLDAP {
// filter
matched, err := ApplyFilter(filterPacket, entry)
// Per https://datatracker.ietf.org/doc/html/rfc4511#section-4.5.1.7
// unimplemented search tags/filters should be ignored. They should not return an error
if err != nil && StatusCode(err) != ldap.LDAPResultFilterError {
return err
}
if !matched {
continue
}
// constrained search scope
switch searchReq.Scope {
case ldap.ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates.
case ldap.ScopeBaseObject: // The scope is constrained to the entry named by baseObject.
if strings.ToLower(entry.DN) != searchReqBaseDNLower {
continue
}
case ldap.ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject.
entryDNLower := strings.ToLower(entry.DN)
parts := strings.Split(entryDNLower, ",")
if len(parts) < 2 && entryDNLower != searchReqBaseDNLower {
continue
}
if dnSuffix := strings.Join(parts[1:], ","); dnSuffix != searchReqBaseDNLower {
continue
}
}
// filter attributes
entry = filterAttributes(entry, searchReq.Attributes)
// size limit
if searchReq.SizeLimit > 0 && i >= searchReq.SizeLimit {
break
}
i++
}
// respond
responsePacket := encodeSearchResponse(messageID, entry)
if err = sendPacket(conn, responsePacket); err != nil {
return ldap.NewError(ldap.LDAPResultOther, err)
}
}
return nil
}
func parseSearchRequest(req *ber.Packet, controls *[]ldap.Control) (ldap.SearchRequest, error) {
if len(req.Children) != 8 {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: invalid length"))
}
// Parse the request
baseObject, ok := req.Children[0].Value.(string)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: base object"))
}
s, ok := req.Children[1].Value.(int64)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: scope"))
}
scope := int(s)
d, ok := req.Children[2].Value.(int64)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: deref aliases"))
}
derefAliases := int(d)
s, ok = req.Children[3].Value.(int64)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: size limit"))
}
sizeLimit := int(s)
t, ok := req.Children[4].Value.(int64)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: time limit"))
}
timeLimit := int(t)
typesOnly := false
if req.Children[5].Value != nil {
typesOnly, ok = req.Children[5].Value.(bool)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("bad search request: types only"))
}
}
filter, err := ldap.DecompileFilter(req.Children[6])
if err != nil {
return ldap.SearchRequest{}, err
}
attributes := []string{}
for _, attr := range req.Children[7].Children {
a, ok := attr.Value.(string)
if !ok {
return ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, fmt.Errorf("bad search request: reading attribute: %v", attr.Value))
}
attributes = append(attributes, a)
}
searchReq := *ldap.NewSearchRequest(baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes, *controls)
return searchReq, nil
}
func filterAttributes(entry *ldap.Entry, attributes []string) *ldap.Entry {
// only return requested attributes
newAttributes := []*ldap.EntryAttribute{}
if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) {
for _, attr := range entry.Attributes {
attrNameLower := strings.ToLower(attr.Name)
for _, requested := range attributes {
requestedLower := strings.ToLower(requested)
// You can request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command.
// "+supportedControl" is treated as an operational attribute
if strings.HasPrefix(attrNameLower, "+") {
if requestedLower == "+" || attrNameLower == "+"+requestedLower {
newAttributes = append(newAttributes, ldap.NewEntryAttribute(attr.Name[1:], attr.Values))
break
}
} else {
if requested == "*" || attrNameLower == requestedLower {
newAttributes = append(newAttributes, attr)
break
}
}
}
}
} else {
// remove operational attributes
for _, attr := range entry.Attributes {
if !strings.HasPrefix(attr.Name, "+") {
newAttributes = append(newAttributes, attr)
}
}
}
entry.Attributes = newAttributes
return entry
}
func encodeSearchResponse(messageID uint64, res *ldap.Entry) *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"))
searchEntry := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultEntry, nil, "Search Result Entry")
searchEntry.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, res.DN, "Object Name"))
attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes:")
for _, attribute := range res.Attributes {
attrs.AppendChild(encodeSearchAttribute(attribute.Name, attribute.Values))
}
searchEntry.AppendChild(attrs)
responsePacket.AppendChild(searchEntry)
return responsePacket
}
func encodeSearchAttribute(name string, values []string) *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, name, "Attribute Name"))
valuesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Attribute Values")
for _, value := range values {
valuesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Attribute Value"))
}
packet.AppendChild(valuesPacket)
return packet
}