From 2ddf265aa00d7d0e1c5d743bc4173277234baf46 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Sun, 22 Nov 2015 17:46:30 -0500 Subject: [PATCH 1/2] Add extensibleMatch filter support --- filter.go | 241 ++++++++++++++++++++++++++++++++++++++++--------- filter_test.go | 54 +++++++++++ 2 files changed, 252 insertions(+), 43 deletions(-) diff --git a/filter.go b/filter.go index 2c1bc38e..47c570b4 100644 --- a/filter.go +++ b/filter.go @@ -53,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{ FilterSubstringsFinal: "Substrings Final", } +const ( + MatchingRuleAssertionMatchingRule = 1 + MatchingRuleAssertionType = 2 + MatchingRuleAssertionMatchValue = 3 + MatchingRuleAssertionDNAttributes = 4 +) + +var MatchingRuleAssertionMap = map[uint64]string{ + MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule", + MatchingRuleAssertionType: "Matching Rule Assertion Type", + MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value", + MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes", +} + func CompileFilter(filter string) (*ber.Packet, error) { if len(filter) == 0 || filter[0] != '(' { 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) { if i == 0 && child.Tag != FilterSubstringsInitial { ret += "*" } - ret += ber.DecodeString(child.Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(child.Data.Bytes())) if child.Tag != FilterSubstringsFinal { ret += "*" } @@ -135,6 +149,37 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) { ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "~=" ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) + case FilterExtensibleMatch: + attr := "" + dnAttributes := false + matchingRule := "" + value := "" + + for _, child := range packet.Children { + switch child.Tag { + case MatchingRuleAssertionMatchingRule: + matchingRule = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionType: + attr = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionMatchValue: + value = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionDNAttributes: + dnAttributes = child.Value.(bool) + } + } + + if len(attr) > 0 { + ret += attr + } + if dnAttributes { + ret += ":dn" + } + if len(matchingRule) > 0 { + ret += ":" + ret += matchingRule + } + ret += ":=" + ret += EscapeFilter(value) } ret += ")" @@ -194,10 +239,20 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { packet.AppendChild(child) return packet, newPos, err default: + READING_ATTR := 0 + READING_EXTENSIBLE_MATCHING_RULE := 1 + READING_CONDITION := 2 + + state := READING_ATTR + attribute := "" + extensibleDNAttributes := false + extensibleMatchingRule := "" condition := "" + for newPos < len(filter) { - currentRune, currentWidth = utf8.DecodeRuneInString(filter[newPos:]) + remainingFilter := filter[newPos:] + currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter) if currentRune == ')' { break } @@ -205,27 +260,86 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) } - nextRune, nextWidth := utf8.DecodeRuneInString(filter[newPos+currentWidth:]) + switch state { + case READING_ATTR: + switch { + // Extensible rule, with only DN-matching + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_CONDITION + newPos += 5 + + // Extensible rule, with DN-matching and a matching OID + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 4 + + // Extensible rule, with attr only + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_CONDITION + newPos += 2 + + // Extensible rule, with no DN attribute matching + case currentRune == ':': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 1 + + // Equality condition + case currentRune == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) + state = READING_CONDITION + newPos += 1 + + // Greater-than or equal + case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Less-than or equal + case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Approx + case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) + state = READING_CONDITION + newPos += 2 + + // Still reading the attribute name + default: + attribute += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_EXTENSIBLE_MATCHING_RULE: + switch { - switch { - case packet != nil: + // Matching rule OID is done + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + state = READING_CONDITION + newPos += 2 + + // Still reading the matching rule oid + default: + extensibleMatchingRule += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_CONDITION: + // append to the condition condition += fmt.Sprintf("%c", currentRune) - case currentRune == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) - case currentRune == '>' && nextRune == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) - newPos += nextWidth // we're skipping the next character as well - case currentRune == '<' && nextRune == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) - newPos += nextWidth // we're skipping the next character as well - case currentRune == '~' && nextRune == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual]) - newPos += nextWidth // we're skipping the next character as well - case packet == nil: - attribute += fmt.Sprintf("%c", currentRune) + newPos += currentWidth } - newPos += currentWidth } + if newPos == len(filter) { err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) return packet, newPos, err @@ -236,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { } switch { + case packet.Tag == FilterExtensibleMatch: + // MatchingRuleAssertion ::= SEQUENCE { + // matchingRule [1] MatchingRuleID OPTIONAL, + // type [2] AttributeDescription OPTIONAL, + // matchValue [3] AssertionValue, + // dnAttributes [4] BOOLEAN DEFAULT FALSE + // } + + // Include the matching rule oid, if specified + if len(extensibleMatchingRule) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) + } + + // Include the attribute, if specified + if len(attribute) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType])) + } + + // Add the value (only required child) + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err + } + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue])) + + // Defaults to false, so only include in the sequence if true + if extensibleDNAttributes { + packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes])) + } + case packet.Tag == FilterEqualityMatch && condition == "*": packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent]) case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"): @@ -257,38 +401,49 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { default: tag = FilterSubstringsAny } - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)])) + encodedString, err := escapedStringToEncodedBytes(part) + if err != nil { + return packet, newPos, err + } + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) } packet.AppendChild(seq) default: - var buffer bytes.Buffer - for i := 0; i < len(condition); i++ { - // Check for escaped hex characters and convert them to their literal value for transport. - if condition[i] == '\\' { - // http://tools.ietf.org/search/rfc4515 - // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not - // being a member of UTF1SUBSET. - if i+2 > len(condition) { - err = NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) - return packet, newPos, err - } - if escByte, decodeErr := hexpac.DecodeString(condition[i+1 : i+3]); decodeErr != nil { - err = NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) - return packet, newPos, err - } else { - buffer.WriteByte(escByte[0]) - i += 2 // +1 from end of loop, so 3 total for \xx. - } - } else { - buffer.WriteString(string(condition[i])) - } + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err } - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, buffer.String(), "Condition")) + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) } newPos += currentWidth return packet, newPos, err } } + +// Convert from "ABC\xx\xx\xx" form to literal bytes for transport +func escapedStringToEncodedBytes(escapedString string) (string, error) { + var buffer bytes.Buffer + for i := 0; i < len(escapedString); i++ { + // Check for escaped hex characters and convert them to their literal value for transport. + if escapedString[i] == '\\' { + // http://tools.ietf.org/search/rfc4515 + // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not + // being a member of UTF1SUBSET. + if i+2 > len(escapedString) { + return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) + } + if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil { + return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) + } else { + buffer.WriteByte(escByte[0]) + i += 2 // +1 from end of loop, so 3 total for \xx. + } + } else { + buffer.WriteString(string(escapedString[i])) + } + } + + return buffer.String(), nil +} diff --git a/filter_test.go b/filter_test.go index 822f215e..e50956f3 100644 --- a/filter_test.go +++ b/filter_test.go @@ -62,6 +62,18 @@ var testFilters = []compileTest{ expectedFilter: "(sn=Mi*l*r)", expectedType: ldap.FilterSubstrings, }, + // substring filters escape properly + compileTest{ + filterStr: `(sn=Mi*함*r)`, + expectedFilter: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, + // already escaped substring filters don't get double-escaped + compileTest{ + filterStr: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, + expectedFilter: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, compileTest{ filterStr: "(sn=Mi*le*)", expectedFilter: "(sn=Mi*le*)", @@ -124,6 +136,48 @@ var testFilters = []compileTest{ expectedFilter: `(&(objectclass=inetorgperson)(cn=\c3\a4\c2\b8\c2\ad\c3\a6\c2\96\c2\87))`, expectedType: 0, }, + // attr extension + compileTest{ + filterStr: `(memberOf:=foo)`, + expectedFilter: `(memberOf:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+named matching rule extension + compileTest{ + filterStr: `(memberOf:test:=foo)`, + expectedFilter: `(memberOf:test:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+oid matching rule extension + compileTest{ + filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn+oid matching rule extension + compileTest{ + filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn extension + compileTest{ + filterStr: `(o:dn:=Ace Industry)`, + expectedFilter: `(o:dn:=Ace Industry)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // dn extension + compileTest{ + filterStr: `(:dn:2.4.6.8.10:=Dino)`, + expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, + expectedType: ldap.FilterExtensibleMatch, + }, + compileTest{ + filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, } From 487fb2b5e14583a9779b12b20067685152bfd9cf Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 23 Nov 2015 01:50:33 -0500 Subject: [PATCH 2/2] Fix string decoding --- filter.go | 15 +++++++++++---- filter_test.go | 12 ++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/filter.go b/filter.go index 47c570b4..63bcec1e 100644 --- a/filter.go +++ b/filter.go @@ -425,9 +425,15 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { // Convert from "ABC\xx\xx\xx" form to literal bytes for transport func escapedStringToEncodedBytes(escapedString string) (string, error) { var buffer bytes.Buffer - for i := 0; i < len(escapedString); i++ { + i := 0 + for i < len(escapedString) { + currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:]) + if currentRune == utf8.RuneError { + return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i)) + } + // Check for escaped hex characters and convert them to their literal value for transport. - if escapedString[i] == '\\' { + if currentRune == '\\' { // http://tools.ietf.org/search/rfc4515 // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not // being a member of UTF1SUBSET. @@ -441,9 +447,10 @@ func escapedStringToEncodedBytes(escapedString string) (string, error) { i += 2 // +1 from end of loop, so 3 total for \xx. } } else { - buffer.WriteString(string(escapedString[i])) + buffer.WriteRune(currentRune) } - } + i += currentWidth + } return buffer.String(), nil } diff --git a/filter_test.go b/filter_test.go index e50956f3..ae1b79b0 100644 --- a/filter_test.go +++ b/filter_test.go @@ -65,13 +65,13 @@ var testFilters = []compileTest{ // substring filters escape properly compileTest{ filterStr: `(sn=Mi*함*r)`, - expectedFilter: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, expectedType: ldap.FilterSubstrings, }, // already escaped substring filters don't get double-escaped compileTest{ - filterStr: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, - expectedFilter: `(sn=Mi*\c3\ad\c2\95\c2\a8*r)`, + filterStr: `(sn=Mi*\ed\95\a8*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, expectedType: ldap.FilterSubstrings, }, compileTest{ @@ -111,12 +111,12 @@ var testFilters = []compileTest{ }, compileTest{ filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`, - 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)`, + 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)`, expectedType: ldap.FilterEqualityMatch, }, compileTest{ filterStr: `(objectGUID=함수목록)`, - 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)`, + expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, expectedType: ldap.FilterEqualityMatch, }, compileTest{ @@ -133,7 +133,7 @@ var testFilters = []compileTest{ }, compileTest{ filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, - expectedFilter: `(&(objectclass=inetorgperson)(cn=\c3\a4\c2\b8\c2\ad\c3\a6\c2\96\c2\87))`, + expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, expectedType: 0, }, // attr extension