-
Notifications
You must be signed in to change notification settings - Fork 371
Add extensibleMatch filter support, fix multi-byte filters #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,38 +239,107 @@ 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 | ||
| } | ||
| if currentRune == utf8.RuneError { | ||
| 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 { | ||
|
|
||
| // Matching rule OID is done | ||
| case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): | ||
| state = READING_CONDITION | ||
| newPos += 2 | ||
|
|
||
| switch { | ||
| case packet != nil: | ||
| // 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,56 @@ 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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. refactored so that substring and extensible match filters can reuse this |
||
| var buffer bytes.Buffer | ||
| 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 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. | ||
| 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.WriteRune(currentRune) | ||
| } | ||
|
|
||
| i += currentWidth | ||
| } | ||
| return buffer.String(), nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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*\ed\95\a8*r)`, | ||
| expectedType: ldap.FilterSubstrings, | ||
| }, | ||
| // already escaped substring filters don't get double-escaped | ||
| compileTest{ | ||
| filterStr: `(sn=Mi*\ed\95\a8*r)`, | ||
| expectedFilter: `(sn=Mi*\ed\95\a8*r)`, | ||
| expectedType: ldap.FilterSubstrings, | ||
| }, | ||
| compileTest{ | ||
| filterStr: "(sn=Mi*le*)", | ||
| expectedFilter: "(sn=Mi*le*)", | ||
|
|
@@ -99,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{ | ||
|
|
@@ -121,9 +133,51 @@ var testFilters = []compileTest{ | |
| }, | ||
| compileTest{ | ||
| filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in case you want to verify the old bytes were wrong, and the new bytes are right: http://play.golang.org/p/GeNmpuSEs7 |
||
| 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 | ||
| 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 }, | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was the line causing multi-byte headaches... should have been write rune on a decoded rune, not an indexed byte
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice catch