Skip to content

Commit e9a325d

Browse files
committed
Merge pull request #43 from liggitt/extensible_match
Add extensibleMatch filter support, fix multi-byte filters
2 parents e983ad5 + 487fb2b commit e9a325d

File tree

2 files changed

+262
-46
lines changed

2 files changed

+262
-46
lines changed

filter.go

Lines changed: 205 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{
5353
FilterSubstringsFinal: "Substrings Final",
5454
}
5555

56+
const (
57+
MatchingRuleAssertionMatchingRule = 1
58+
MatchingRuleAssertionType = 2
59+
MatchingRuleAssertionMatchValue = 3
60+
MatchingRuleAssertionDNAttributes = 4
61+
)
62+
63+
var MatchingRuleAssertionMap = map[uint64]string{
64+
MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule",
65+
MatchingRuleAssertionType: "Matching Rule Assertion Type",
66+
MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value",
67+
MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes",
68+
}
69+
5670
func CompileFilter(filter string) (*ber.Packet, error) {
5771
if len(filter) == 0 || filter[0] != '(' {
5872
return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('"))
@@ -111,7 +125,7 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
111125
if i == 0 && child.Tag != FilterSubstringsInitial {
112126
ret += "*"
113127
}
114-
ret += ber.DecodeString(child.Data.Bytes())
128+
ret += EscapeFilter(ber.DecodeString(child.Data.Bytes()))
115129
if child.Tag != FilterSubstringsFinal {
116130
ret += "*"
117131
}
@@ -135,6 +149,37 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
135149
ret += ber.DecodeString(packet.Children[0].Data.Bytes())
136150
ret += "~="
137151
ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
152+
case FilterExtensibleMatch:
153+
attr := ""
154+
dnAttributes := false
155+
matchingRule := ""
156+
value := ""
157+
158+
for _, child := range packet.Children {
159+
switch child.Tag {
160+
case MatchingRuleAssertionMatchingRule:
161+
matchingRule = ber.DecodeString(child.Data.Bytes())
162+
case MatchingRuleAssertionType:
163+
attr = ber.DecodeString(child.Data.Bytes())
164+
case MatchingRuleAssertionMatchValue:
165+
value = ber.DecodeString(child.Data.Bytes())
166+
case MatchingRuleAssertionDNAttributes:
167+
dnAttributes = child.Value.(bool)
168+
}
169+
}
170+
171+
if len(attr) > 0 {
172+
ret += attr
173+
}
174+
if dnAttributes {
175+
ret += ":dn"
176+
}
177+
if len(matchingRule) > 0 {
178+
ret += ":"
179+
ret += matchingRule
180+
}
181+
ret += ":="
182+
ret += EscapeFilter(value)
138183
}
139184

140185
ret += ")"
@@ -194,38 +239,107 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
194239
packet.AppendChild(child)
195240
return packet, newPos, err
196241
default:
242+
READING_ATTR := 0
243+
READING_EXTENSIBLE_MATCHING_RULE := 1
244+
READING_CONDITION := 2
245+
246+
state := READING_ATTR
247+
197248
attribute := ""
249+
extensibleDNAttributes := false
250+
extensibleMatchingRule := ""
198251
condition := ""
252+
199253
for newPos < len(filter) {
200-
currentRune, currentWidth = utf8.DecodeRuneInString(filter[newPos:])
254+
remainingFilter := filter[newPos:]
255+
currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter)
201256
if currentRune == ')' {
202257
break
203258
}
204259
if currentRune == utf8.RuneError {
205260
return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos))
206261
}
207262

208-
nextRune, nextWidth := utf8.DecodeRuneInString(filter[newPos+currentWidth:])
263+
switch state {
264+
case READING_ATTR:
265+
switch {
266+
// Extensible rule, with only DN-matching
267+
case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="):
268+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
269+
extensibleDNAttributes = true
270+
state = READING_CONDITION
271+
newPos += 5
272+
273+
// Extensible rule, with DN-matching and a matching OID
274+
case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"):
275+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
276+
extensibleDNAttributes = true
277+
state = READING_EXTENSIBLE_MATCHING_RULE
278+
newPos += 4
279+
280+
// Extensible rule, with attr only
281+
case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
282+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
283+
state = READING_CONDITION
284+
newPos += 2
285+
286+
// Extensible rule, with no DN attribute matching
287+
case currentRune == ':':
288+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
289+
state = READING_EXTENSIBLE_MATCHING_RULE
290+
newPos += 1
291+
292+
// Equality condition
293+
case currentRune == '=':
294+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
295+
state = READING_CONDITION
296+
newPos += 1
297+
298+
// Greater-than or equal
299+
case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="):
300+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
301+
state = READING_CONDITION
302+
newPos += 2
303+
304+
// Less-than or equal
305+
case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="):
306+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
307+
state = READING_CONDITION
308+
newPos += 2
309+
310+
// Approx
311+
case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="):
312+
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch])
313+
state = READING_CONDITION
314+
newPos += 2
315+
316+
// Still reading the attribute name
317+
default:
318+
attribute += fmt.Sprintf("%c", currentRune)
319+
newPos += currentWidth
320+
}
321+
322+
case READING_EXTENSIBLE_MATCHING_RULE:
323+
switch {
324+
325+
// Matching rule OID is done
326+
case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
327+
state = READING_CONDITION
328+
newPos += 2
209329

210-
switch {
211-
case packet != nil:
330+
// Still reading the matching rule oid
331+
default:
332+
extensibleMatchingRule += fmt.Sprintf("%c", currentRune)
333+
newPos += currentWidth
334+
}
335+
336+
case READING_CONDITION:
337+
// append to the condition
212338
condition += fmt.Sprintf("%c", currentRune)
213-
case currentRune == '=':
214-
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
215-
case currentRune == '>' && nextRune == '=':
216-
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
217-
newPos += nextWidth // we're skipping the next character as well
218-
case currentRune == '<' && nextRune == '=':
219-
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
220-
newPos += nextWidth // we're skipping the next character as well
221-
case currentRune == '~' && nextRune == '=':
222-
packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual])
223-
newPos += nextWidth // we're skipping the next character as well
224-
case packet == nil:
225-
attribute += fmt.Sprintf("%c", currentRune)
339+
newPos += currentWidth
226340
}
227-
newPos += currentWidth
228341
}
342+
229343
if newPos == len(filter) {
230344
err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter"))
231345
return packet, newPos, err
@@ -236,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
236350
}
237351

238352
switch {
353+
case packet.Tag == FilterExtensibleMatch:
354+
// MatchingRuleAssertion ::= SEQUENCE {
355+
// matchingRule [1] MatchingRuleID OPTIONAL,
356+
// type [2] AttributeDescription OPTIONAL,
357+
// matchValue [3] AssertionValue,
358+
// dnAttributes [4] BOOLEAN DEFAULT FALSE
359+
// }
360+
361+
// Include the matching rule oid, if specified
362+
if len(extensibleMatchingRule) > 0 {
363+
packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule]))
364+
}
365+
366+
// Include the attribute, if specified
367+
if len(attribute) > 0 {
368+
packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType]))
369+
}
370+
371+
// Add the value (only required child)
372+
encodedString, err := escapedStringToEncodedBytes(condition)
373+
if err != nil {
374+
return packet, newPos, err
375+
}
376+
packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue]))
377+
378+
// Defaults to false, so only include in the sequence if true
379+
if extensibleDNAttributes {
380+
packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes]))
381+
}
382+
239383
case packet.Tag == FilterEqualityMatch && condition == "*":
240384
packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent])
241385
case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"):
@@ -257,38 +401,56 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
257401
default:
258402
tag = FilterSubstringsAny
259403
}
260-
seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)]))
404+
encodedString, err := escapedStringToEncodedBytes(part)
405+
if err != nil {
406+
return packet, newPos, err
407+
}
408+
seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)]))
261409
}
262410
packet.AppendChild(seq)
263411
default:
264-
var buffer bytes.Buffer
265-
for i := 0; i < len(condition); i++ {
266-
// Check for escaped hex characters and convert them to their literal value for transport.
267-
if condition[i] == '\\' {
268-
// http://tools.ietf.org/search/rfc4515
269-
// \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not
270-
// being a member of UTF1SUBSET.
271-
if i+2 > len(condition) {
272-
err = NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter"))
273-
return packet, newPos, err
274-
}
275-
if escByte, decodeErr := hexpac.DecodeString(condition[i+1 : i+3]); decodeErr != nil {
276-
err = NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter"))
277-
return packet, newPos, err
278-
} else {
279-
buffer.WriteByte(escByte[0])
280-
i += 2 // +1 from end of loop, so 3 total for \xx.
281-
}
282-
} else {
283-
buffer.WriteString(string(condition[i]))
284-
}
412+
encodedString, err := escapedStringToEncodedBytes(condition)
413+
if err != nil {
414+
return packet, newPos, err
285415
}
286-
287416
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute"))
288-
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, buffer.String(), "Condition"))
417+
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition"))
289418
}
290419

291420
newPos += currentWidth
292421
return packet, newPos, err
293422
}
294423
}
424+
425+
// Convert from "ABC\xx\xx\xx" form to literal bytes for transport
426+
func escapedStringToEncodedBytes(escapedString string) (string, error) {
427+
var buffer bytes.Buffer
428+
i := 0
429+
for i < len(escapedString) {
430+
currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:])
431+
if currentRune == utf8.RuneError {
432+
return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i))
433+
}
434+
435+
// Check for escaped hex characters and convert them to their literal value for transport.
436+
if currentRune == '\\' {
437+
// http://tools.ietf.org/search/rfc4515
438+
// \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not
439+
// being a member of UTF1SUBSET.
440+
if i+2 > len(escapedString) {
441+
return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter"))
442+
}
443+
if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil {
444+
return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter"))
445+
} else {
446+
buffer.WriteByte(escByte[0])
447+
i += 2 // +1 from end of loop, so 3 total for \xx.
448+
}
449+
} else {
450+
buffer.WriteRune(currentRune)
451+
}
452+
453+
i += currentWidth
454+
}
455+
return buffer.String(), nil
456+
}

filter_test.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ var testFilters = []compileTest{
6262
expectedFilter: "(sn=Mi*l*r)",
6363
expectedType: ldap.FilterSubstrings,
6464
},
65+
// substring filters escape properly
66+
compileTest{
67+
filterStr: `(sn=Mi*함*r)`,
68+
expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
69+
expectedType: ldap.FilterSubstrings,
70+
},
71+
// already escaped substring filters don't get double-escaped
72+
compileTest{
73+
filterStr: `(sn=Mi*\ed\95\a8*r)`,
74+
expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
75+
expectedType: ldap.FilterSubstrings,
76+
},
6577
compileTest{
6678
filterStr: "(sn=Mi*le*)",
6779
expectedFilter: "(sn=Mi*le*)",
@@ -99,12 +111,12 @@ var testFilters = []compileTest{
99111
},
100112
compileTest{
101113
filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`,
102-
expectedFilter: `(objectGUID=\c3\90\c2\b0\c3\90\c2\b1\c3\90\c2\b2\c3\90\c2\b3\c3\90\c2\b4\c3\90\c2\b5\c3\91\c2\91\c3\90\c2\b6\c3\90\c2\b7\c3\90\c2\b8\c3\90\c2\b9\c3\90\c2\ba\c3\90\c2\bb\c3\90\c2\bc\c3\90\c2\bd\c3\90\c2\be\c3\90\c2\bf\c3\91\c2\80\c3\91\c2\81\c3\91\c2\82\c3\91\c2\83\c3\91\c2\84\c3\91\c2\85\c3\91\c2\86\c3\91\c2\87\c3\91\c2\88\c3\91\c2\89\c3\91\c2\8a\c3\91\c2\8b\c3\91\c2\8c\c3\91\c2\8d\c3\91\c2\8e\c3\91\c2\8f)`,
114+
expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`,
103115
expectedType: ldap.FilterEqualityMatch,
104116
},
105117
compileTest{
106118
filterStr: `(objectGUID=함수목록)`,
107-
expectedFilter: `(objectGUID=\c3\ad\c2\95\c2\a8\c3\ac\c2\88\c2\98\c3\ab\c2\aa\c2\a9\c3\ab\c2\a1\c2\9d)`,
119+
expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`,
108120
expectedType: ldap.FilterEqualityMatch,
109121
},
110122
compileTest{
@@ -121,9 +133,51 @@ var testFilters = []compileTest{
121133
},
122134
compileTest{
123135
filterStr: `(&(objectclass=inetorgperson)(cn=中文))`,
124-
expectedFilter: `(&(objectclass=inetorgperson)(cn=\c3\a4\c2\b8\c2\ad\c3\a6\c2\96\c2\87))`,
136+
expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`,
125137
expectedType: 0,
126138
},
139+
// attr extension
140+
compileTest{
141+
filterStr: `(memberOf:=foo)`,
142+
expectedFilter: `(memberOf:=foo)`,
143+
expectedType: ldap.FilterExtensibleMatch,
144+
},
145+
// attr+named matching rule extension
146+
compileTest{
147+
filterStr: `(memberOf:test:=foo)`,
148+
expectedFilter: `(memberOf:test:=foo)`,
149+
expectedType: ldap.FilterExtensibleMatch,
150+
},
151+
// attr+oid matching rule extension
152+
compileTest{
153+
filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`,
154+
expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`,
155+
expectedType: ldap.FilterExtensibleMatch,
156+
},
157+
// attr+dn+oid matching rule extension
158+
compileTest{
159+
filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
160+
expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
161+
expectedType: ldap.FilterExtensibleMatch,
162+
},
163+
// attr+dn extension
164+
compileTest{
165+
filterStr: `(o:dn:=Ace Industry)`,
166+
expectedFilter: `(o:dn:=Ace Industry)`,
167+
expectedType: ldap.FilterExtensibleMatch,
168+
},
169+
// dn extension
170+
compileTest{
171+
filterStr: `(:dn:2.4.6.8.10:=Dino)`,
172+
expectedFilter: `(:dn:2.4.6.8.10:=Dino)`,
173+
expectedType: ldap.FilterExtensibleMatch,
174+
},
175+
compileTest{
176+
filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
177+
expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
178+
expectedType: ldap.FilterExtensibleMatch,
179+
},
180+
127181
// compileTest{ filterStr: "()", filterType: FilterExtensibleMatch },
128182
}
129183

0 commit comments

Comments
 (0)