Skip to content

Commit 10c3753

Browse files
committed
feat: add support for escaped characters in searches
1 parent 5c8c5f6 commit 10c3753

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

src/ldap/request/filter.cr

+46-7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ class LDAP::Request::Filter
1414
Extensible
1515
end
1616

17+
ESCAPES = {
18+
"\0" => "\\00", # NUL = %x00 ; null character
19+
"*" => "\\2A", # ASTERISK = %x2A ; asterisk ("*")
20+
"(" => "\\28", # LPARENS = %x28 ; left parenthesis ("(")
21+
")" => "\\29", # RPARENS = %x29 ; right parenthesis (")")
22+
"\\" => "\\5C", # ESC = %x5C ; esc (or backslash) ("\")
23+
}
24+
# Compiled character class regexp using the keys from the above hash.
25+
ESCAPE_RE = Regex.new(String.build { |str|
26+
str << "["
27+
ESCAPES.keys.each { |e| str << Regex.escape(e) }
28+
str << "]"
29+
})
30+
31+
# Escape a string for use in an LDAP filter
32+
def self.escape(string : String) : String
33+
string.gsub(ESCAPE_RE) { |char| ESCAPES[char] }
34+
end
35+
36+
def self.unescape(value : String, escaped : Bool = false) : String
37+
escaped ? value.gsub(/\\([a-fA-F\d]{2})/) { String.new($1.hexbytes) } : value
38+
end
39+
1740
def initialize(@operation : Type, @filter : BER)
1841
end
1942

@@ -27,7 +50,7 @@ class LDAP::Request::Filter
2750
to_ber.to_slice
2851
end
2952

30-
def self.equal(object : String, value)
53+
def self.equal(object : String, value, escaped : Bool = false)
3154
value = value.to_s
3255
if value == "*"
3356
self.new(Type::Equal, BER.new.set_string(object, 7, TagClass::ContextSpecific))
@@ -38,17 +61,17 @@ class LDAP::Request::Filter
3861
first = nil
3962
ary.shift
4063
else
41-
first = BER.new.set_string(ary.shift, 0, TagClass::ContextSpecific)
64+
first = BER.new.set_string(unescape(ary.shift, escaped), 0, TagClass::ContextSpecific)
4265
end
4366

4467
if ary.last.empty?
4568
last = nil
4669
ary.pop
4770
else
48-
last = BER.new.set_string(ary.pop, 2, TagClass::ContextSpecific)
71+
last = BER.new.set_string(unescape(ary.pop, escaped), 2, TagClass::ContextSpecific)
4972
end
5073

51-
seq = ary.map { |e| BER.new.set_string(e, 1, TagClass::ContextSpecific) }
74+
seq = ary.map { |e| BER.new.set_string(unescape(e, escaped), 1, TagClass::ContextSpecific) }
5275
seq.unshift first if first
5376
seq.push last if last
5477
seq = LDAP.sequence(seq)
@@ -57,7 +80,7 @@ class LDAP::Request::Filter
5780
self.new(Type::Equal, LDAP.context_sequence({left, seq}, 4))
5881
else
5982
left = BER.new.set_string(object, UniversalTags::OctetString)
60-
right = BER.new.set_string(value, UniversalTags::OctetString)
83+
right = BER.new.set_string(unescape(value, escaped), UniversalTags::OctetString)
6184
self.new(Type::Equal, LDAP.context_sequence({left, right}, 3))
6285
end
6386
end
@@ -74,10 +97,10 @@ class LDAP::Request::Filter
7497
self.new(Type::LessThanOrEqual, LDAP.context_sequence({left, right}, 6))
7598
end
7699

77-
def self.not_equal(object : String, value)
100+
def self.not_equal(object : String, value, escaped : Bool = false)
78101
self.new(
79102
Type::NotEqual,
80-
LDAP.context_sequence({equal(object, value).to_ber}, 2)
103+
LDAP.context_sequence({equal(object, value, escaped).to_ber}, 2)
81104
)
82105
end
83106

@@ -105,6 +128,22 @@ class LDAP::Request::Filter
105128
self.new(Type::Or, LDAP.context_sequence({left.to_ber, right.to_ber}, 1))
106129
end
107130

131+
def self.begins(attribute : String, value)
132+
equal(attribute, "#{escape(value.to_s)}*", escaped: true)
133+
end
134+
135+
def self.ends(attribute : String, value)
136+
equal(attribute, "*#{escape(value.to_s)}", escaped: true)
137+
end
138+
139+
def self.contains(attribute : String, value)
140+
equal(attribute, "*#{escape(value.to_s)}*", escaped: true)
141+
end
142+
143+
def self.present?(attribute : String)
144+
equal(attribute, "*")
145+
end
146+
108147
# Joins two or more filters so that all conditions must be true.
109148
def &(filter)
110149
self.class.join(self, filter)

src/ldap/request/filter_parser.cr

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class LDAP::Request::FilterParser
3636
value = value.strip
3737
case op
3838
when "="
39-
Filter.equal(token, value)
39+
Filter.equal(token, value, escaped: true)
4040
when "!="
41-
Filter.not_equal(token, value)
41+
Filter.not_equal(token, value, escaped: true)
4242
when "<="
4343
Filter.less_than(token, value)
4444
when ">="

0 commit comments

Comments
 (0)