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}