package ldap import ( "bytes" "context" "crypto/rand" "net" "strconv" "strings" "sync" fk "tug/internal/transport/ldap/internal/glauthldap" ) const ( resultCap = 5000 cursorsPerConn = 4 scopeBaseObject = 0 scopeSingleLevel = 1 scopeWholeSubtree = 2 ) type cursor struct { cookie string fingerprint []byte entries []*entry final fk.LDAPResultCode } type cursors struct { mu sync.Mutex byConn map[net.Conn][]*cursor } func (c *cursors) take(conn net.Conn, cookie string) *cursor { c.mu.Lock() defer c.mu.Unlock() list := c.byConn[conn] for i, cur := range list { if cur.cookie == cookie { c.byConn[conn] = append(list[:i:i], list[i+1:]...) return cur } } return nil } func (c *cursors) put(conn net.Conn, cur *cursor) { c.mu.Lock() defer c.mu.Unlock() list := append(c.byConn[conn], cur) if len(list) > cursorsPerConn { list = list[len(list)-cursorsPerConn:] } c.byConn[conn] = list } func (c *cursors) drop(conn net.Conn) { c.mu.Lock() defer c.mu.Unlock() delete(c.byConn, conn) } func newCookie() string { b := make([]byte, 16) rand.Read(b) return string(b) } func fingerprint(base string, req fk.StructuredSearchRequest, f FilterExpr) []byte { var b bytes.Buffer b.WriteString(base) b.WriteByte(0) b.WriteString(strconv.Itoa(req.Scope)) b.WriteByte(0) b.WriteString(strconv.Itoa(req.DerefAliases)) b.WriteByte(0) b.WriteString(strconv.Itoa(req.SizeLimit)) b.WriteByte(0) b.WriteString(strconv.Itoa(req.TimeLimit)) b.WriteByte(0) b.WriteString(strconv.FormatBool(req.TypesOnly)) b.WriteByte(0) for _, a := range req.Attributes { b.WriteString(strings.ToLower(a)) b.WriteByte(1) } b.WriteByte(0) b.Write(f.canonical()) return b.Bytes() } func inScope(dn, base string, scope int) bool { switch scope { case scopeBaseObject: return dn == base case scopeSingleLevel: rest, ok := strings.CutSuffix(dn, ","+base) return ok && !strings.Contains(rest, ",") case scopeWholeSubtree: return dn == base || strings.HasSuffix(dn, ","+base) } return false } func project(e *entry, attributes []string, typesOnly bool) *fk.Entry { all, operational := len(attributes) == 0, false named := map[string]bool{} for _, a := range attributes { switch a { case "*": all = true case "+": operational = true case "1.1": default: named[strings.ToLower(a)] = true } } out := &fk.Entry{DN: e.dn} for _, an := range attributeNames { values, ok := e.attrs[an.key] if !ok || len(values) == 0 { continue } op := operationalAttributes[an.key] if !named[an.key] && !(all && !op) && !(operational && op) { continue } attr := &fk.EntryAttribute{Name: an.name} if !typesOnly { attr.Values = append([]string(nil), values...) } out.Attributes = append(out.Attributes, attr) } return out } func (s *Server) searchStructured(boundDN string, req fk.StructuredSearchRequest, conn net.Conn) (fk.ServerSearchResult, error) { if boundDN == "" { return fk.ServerSearchResult{ResultCode: fk.LDAPResultInsufficientAccessRights}, nil } var paging *fk.ControlPaging for _, c := range req.Controls { switch c := c.(type) { case *fk.ControlPaging: paging = c case *fk.ControlString: if c.Criticality { return fk.ServerSearchResult{ResultCode: fk.LDAPResultUnavailableCriticalExtension}, nil } } } f, err := Copy(req.Filter) if err != nil { return fk.ServerSearchResult{ResultCode: fk.LDAPResultProtocolError}, nil } base, err := normalizeDN(req.BaseDN) if err != nil { return fk.ServerSearchResult{ResultCode: fk.LDAPResultNoSuchObject}, nil } if paging != nil && req.SizeLimit > 0 && int(paging.PagingSize) >= req.SizeLimit { paging = nil } if paging != nil && len(paging.Cookie) > 0 { return s.continuePage(conn, base, req, f, paging) } matches, final, code := s.match(base, req, f) if code != fk.LDAPResultSuccess { return fk.ServerSearchResult{ResultCode: code}, nil } if paging == nil { return fk.ServerSearchResult{Entries: s.render(matches, req), ResultCode: final}, nil } size := int(paging.PagingSize) if size == 0 { return pageResult(nil, nil, fk.LDAPResultSuccess), nil } if len(matches) <= size { return pageResult(s.render(matches, req), nil, final), nil } cur := &cursor{cookie: newCookie(), fingerprint: fingerprint(base, req, f), entries: matches[size:], final: final} s.cursors.put(conn, cur) return pageResult(s.render(matches[:size], req), []byte(cur.cookie), fk.LDAPResultSuccess), nil } func pageResult(entries []*fk.Entry, cookie []byte, code fk.LDAPResultCode) fk.ServerSearchResult { return fk.ServerSearchResult{ Entries: entries, ResultCode: code, Controls: []fk.Control{&fk.ControlPaging{Cookie: cookie}}, } } func (s *Server) continuePage(conn net.Conn, base string, req fk.StructuredSearchRequest, f FilterExpr, paging *fk.ControlPaging) (fk.ServerSearchResult, error) { cur := s.cursors.take(conn, string(paging.Cookie)) if cur == nil || !bytes.Equal(cur.fingerprint, fingerprint(base, req, f)) { return fk.ServerSearchResult{ResultCode: fk.LDAPResultUnwillingToPerform}, nil } size := int(paging.PagingSize) if size == 0 { return pageResult(nil, nil, fk.LDAPResultSuccess), nil } if len(cur.entries) <= size { return pageResult(s.render(cur.entries, req), nil, cur.final), nil } page := cur.entries[:size] cur.entries = cur.entries[size:] cur.cookie = newCookie() s.cursors.put(conn, cur) return pageResult(s.render(page, req), []byte(cur.cookie), fk.LDAPResultSuccess), nil } func (s *Server) match(base string, req fk.StructuredSearchRequest, f FilterExpr) ([]*entry, fk.LDAPResultCode, fk.LDAPResultCode) { if base == "" { if req.Scope != scopeBaseObject { return nil, 0, fk.LDAPResultNoSuchObject } root := s.dir.rootDSE() if f.eval(root) == triTrue { return []*entry{root}, fk.LDAPResultSuccess, fk.LDAPResultSuccess } return nil, fk.LDAPResultSuccess, fk.LDAPResultSuccess } ctx, cancel := context.WithTimeout(s.ctx, operationTimeout) defer cancel() snap, err := s.dir.snapshot(ctx) if err != nil { s.logger.Warn("ldap search snapshot failed", "reason", "store") return nil, 0, fk.LDAPResultUnavailable } if _, ok := snap.byDN[base]; !ok { return nil, 0, fk.LDAPResultNoSuchObject } var out []*entry for _, e := range snap.entries { if inScope(e.dn, base, req.Scope) && f.eval(e) == triTrue { out = append(out, e) } } var final fk.LDAPResultCode = fk.LDAPResultSuccess if len(out) > resultCap { out, final = out[:resultCap], fk.LDAPResultAdminLimitExceeded } if req.SizeLimit > 0 && len(out) > req.SizeLimit { out, final = out[:req.SizeLimit], fk.LDAPResultSizeLimitExceeded } return out, final, fk.LDAPResultSuccess } func (s *Server) render(entries []*entry, req fk.StructuredSearchRequest) []*fk.Entry { out := make([]*fk.Entry, len(entries)) for i, e := range entries { out[i] = project(e, req.Attributes, req.TypesOnly) } return out }