Raw
  1package ldap
  2
  3import (
  4	"bytes"
  5	"context"
  6	"crypto/rand"
  7	"net"
  8	"strconv"
  9	"strings"
 10	"sync"
 11
 12	fk "tug/internal/transport/ldap/internal/glauthldap"
 13)
 14
 15const (
 16	resultCap         = 5000
 17	cursorsPerConn    = 4
 18	scopeBaseObject   = 0
 19	scopeSingleLevel  = 1
 20	scopeWholeSubtree = 2
 21)
 22
 23type cursor struct {
 24	cookie      string
 25	fingerprint []byte
 26	entries     []*entry
 27	final       fk.LDAPResultCode
 28}
 29
 30type cursors struct {
 31	mu     sync.Mutex
 32	byConn map[net.Conn][]*cursor
 33}
 34
 35func (c *cursors) take(conn net.Conn, cookie string) *cursor {
 36	c.mu.Lock()
 37	defer c.mu.Unlock()
 38	list := c.byConn[conn]
 39	for i, cur := range list {
 40		if cur.cookie == cookie {
 41			c.byConn[conn] = append(list[:i:i], list[i+1:]...)
 42			return cur
 43		}
 44	}
 45	return nil
 46}
 47
 48func (c *cursors) put(conn net.Conn, cur *cursor) {
 49	c.mu.Lock()
 50	defer c.mu.Unlock()
 51	list := append(c.byConn[conn], cur)
 52	if len(list) > cursorsPerConn {
 53		list = list[len(list)-cursorsPerConn:]
 54	}
 55	c.byConn[conn] = list
 56}
 57
 58func (c *cursors) drop(conn net.Conn) {
 59	c.mu.Lock()
 60	defer c.mu.Unlock()
 61	delete(c.byConn, conn)
 62}
 63
 64func newCookie() string {
 65	b := make([]byte, 16)
 66	rand.Read(b)
 67	return string(b)
 68}
 69
 70func fingerprint(base string, req fk.StructuredSearchRequest, f FilterExpr) []byte {
 71	var b bytes.Buffer
 72	b.WriteString(base)
 73	b.WriteByte(0)
 74	b.WriteString(strconv.Itoa(req.Scope))
 75	b.WriteByte(0)
 76	b.WriteString(strconv.Itoa(req.DerefAliases))
 77	b.WriteByte(0)
 78	b.WriteString(strconv.Itoa(req.SizeLimit))
 79	b.WriteByte(0)
 80	b.WriteString(strconv.Itoa(req.TimeLimit))
 81	b.WriteByte(0)
 82	b.WriteString(strconv.FormatBool(req.TypesOnly))
 83	b.WriteByte(0)
 84	for _, a := range req.Attributes {
 85		b.WriteString(strings.ToLower(a))
 86		b.WriteByte(1)
 87	}
 88	b.WriteByte(0)
 89	b.Write(f.canonical())
 90	return b.Bytes()
 91}
 92
 93func inScope(dn, base string, scope int) bool {
 94	switch scope {
 95	case scopeBaseObject:
 96		return dn == base
 97	case scopeSingleLevel:
 98		rest, ok := strings.CutSuffix(dn, ","+base)
 99		return ok && !strings.Contains(rest, ",")
100	case scopeWholeSubtree:
101		return dn == base || strings.HasSuffix(dn, ","+base)
102	}
103	return false
104}
105
106func project(e *entry, attributes []string, typesOnly bool) *fk.Entry {
107	all, operational := len(attributes) == 0, false
108	named := map[string]bool{}
109	for _, a := range attributes {
110		switch a {
111		case "*":
112			all = true
113		case "+":
114			operational = true
115		case "1.1":
116		default:
117			named[strings.ToLower(a)] = true
118		}
119	}
120	out := &fk.Entry{DN: e.dn}
121	for _, an := range attributeNames {
122		values, ok := e.attrs[an.key]
123		if !ok || len(values) == 0 {
124			continue
125		}
126		op := operationalAttributes[an.key]
127		if !named[an.key] && !(all && !op) && !(operational && op) {
128			continue
129		}
130		attr := &fk.EntryAttribute{Name: an.name}
131		if !typesOnly {
132			attr.Values = append([]string(nil), values...)
133		}
134		out.Attributes = append(out.Attributes, attr)
135	}
136	return out
137}
138
139func (s *Server) searchStructured(boundDN string, req fk.StructuredSearchRequest, conn net.Conn) (fk.ServerSearchResult, error) {
140	if boundDN == "" {
141		return fk.ServerSearchResult{ResultCode: fk.LDAPResultInsufficientAccessRights}, nil
142	}
143	var paging *fk.ControlPaging
144	for _, c := range req.Controls {
145		switch c := c.(type) {
146		case *fk.ControlPaging:
147			paging = c
148		case *fk.ControlString:
149			if c.Criticality {
150				return fk.ServerSearchResult{ResultCode: fk.LDAPResultUnavailableCriticalExtension}, nil
151			}
152		}
153	}
154	f, err := Copy(req.Filter)
155	if err != nil {
156		return fk.ServerSearchResult{ResultCode: fk.LDAPResultProtocolError}, nil
157	}
158	base, err := normalizeDN(req.BaseDN)
159	if err != nil {
160		return fk.ServerSearchResult{ResultCode: fk.LDAPResultNoSuchObject}, nil
161	}
162	if paging != nil && req.SizeLimit > 0 && int(paging.PagingSize) >= req.SizeLimit {
163		paging = nil
164	}
165	if paging != nil && len(paging.Cookie) > 0 {
166		return s.continuePage(conn, base, req, f, paging)
167	}
168	matches, final, code := s.match(base, req, f)
169	if code != fk.LDAPResultSuccess {
170		return fk.ServerSearchResult{ResultCode: code}, nil
171	}
172	if paging == nil {
173		return fk.ServerSearchResult{Entries: s.render(matches, req), ResultCode: final}, nil
174	}
175	size := int(paging.PagingSize)
176	if size == 0 {
177		return pageResult(nil, nil, fk.LDAPResultSuccess), nil
178	}
179	if len(matches) <= size {
180		return pageResult(s.render(matches, req), nil, final), nil
181	}
182	cur := &cursor{cookie: newCookie(), fingerprint: fingerprint(base, req, f), entries: matches[size:], final: final}
183	s.cursors.put(conn, cur)
184	return pageResult(s.render(matches[:size], req), []byte(cur.cookie), fk.LDAPResultSuccess), nil
185}
186
187func pageResult(entries []*fk.Entry, cookie []byte, code fk.LDAPResultCode) fk.ServerSearchResult {
188	return fk.ServerSearchResult{
189		Entries:    entries,
190		ResultCode: code,
191		Controls:   []fk.Control{&fk.ControlPaging{Cookie: cookie}},
192	}
193}
194
195func (s *Server) continuePage(conn net.Conn, base string, req fk.StructuredSearchRequest, f FilterExpr, paging *fk.ControlPaging) (fk.ServerSearchResult, error) {
196	cur := s.cursors.take(conn, string(paging.Cookie))
197	if cur == nil || !bytes.Equal(cur.fingerprint, fingerprint(base, req, f)) {
198		return fk.ServerSearchResult{ResultCode: fk.LDAPResultUnwillingToPerform}, nil
199	}
200	size := int(paging.PagingSize)
201	if size == 0 {
202		return pageResult(nil, nil, fk.LDAPResultSuccess), nil
203	}
204	if len(cur.entries) <= size {
205		return pageResult(s.render(cur.entries, req), nil, cur.final), nil
206	}
207	page := cur.entries[:size]
208	cur.entries = cur.entries[size:]
209	cur.cookie = newCookie()
210	s.cursors.put(conn, cur)
211	return pageResult(s.render(page, req), []byte(cur.cookie), fk.LDAPResultSuccess), nil
212}
213
214func (s *Server) match(base string, req fk.StructuredSearchRequest, f FilterExpr) ([]*entry, fk.LDAPResultCode, fk.LDAPResultCode) {
215	if base == "" {
216		if req.Scope != scopeBaseObject {
217			return nil, 0, fk.LDAPResultNoSuchObject
218		}
219		root := s.dir.rootDSE()
220		if f.eval(root) == triTrue {
221			return []*entry{root}, fk.LDAPResultSuccess, fk.LDAPResultSuccess
222		}
223		return nil, fk.LDAPResultSuccess, fk.LDAPResultSuccess
224	}
225	ctx, cancel := context.WithTimeout(s.ctx, operationTimeout)
226	defer cancel()
227	snap, err := s.dir.snapshot(ctx)
228	if err != nil {
229		s.logger.Warn("ldap search snapshot failed", "reason", "store")
230		return nil, 0, fk.LDAPResultUnavailable
231	}
232	if _, ok := snap.byDN[base]; !ok {
233		return nil, 0, fk.LDAPResultNoSuchObject
234	}
235	var out []*entry
236	for _, e := range snap.entries {
237		if inScope(e.dn, base, req.Scope) && f.eval(e) == triTrue {
238			out = append(out, e)
239		}
240	}
241	var final fk.LDAPResultCode = fk.LDAPResultSuccess
242	if len(out) > resultCap {
243		out, final = out[:resultCap], fk.LDAPResultAdminLimitExceeded
244	}
245	if req.SizeLimit > 0 && len(out) > req.SizeLimit {
246		out, final = out[:req.SizeLimit], fk.LDAPResultSizeLimitExceeded
247	}
248	return out, final, fk.LDAPResultSuccess
249}
250
251func (s *Server) render(entries []*entry, req fk.StructuredSearchRequest) []*fk.Entry {
252	out := make([]*fk.Entry, len(entries))
253	for i, e := range entries {
254		out[i] = project(e, req.Attributes, req.TypesOnly)
255	}
256	return out
257}