net-ldap 0.0.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of net-ldap might be problematic. Click here for more details.

@@ -0,0 +1,108 @@
1
+ # $Id$
2
+ #
3
+ #
4
+ #----------------------------------------------------------------------------
5
+ #
6
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7
+ #
8
+ # Gmail: garbagecat10
9
+ #
10
+ # This program is free software; you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation; either version 2 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23
+ #
24
+ #---------------------------------------------------------------------------
25
+ #
26
+ #
27
+
28
+
29
+
30
+
31
+ module Net
32
+ class LDAP
33
+
34
+ class Dataset < Hash
35
+
36
+ attr_reader :comments
37
+
38
+
39
+ def Dataset::read_ldif io
40
+ ds = Dataset.new
41
+
42
+ line = io.gets && chomp
43
+ dn = nil
44
+
45
+ while line
46
+ io.gets and chomp
47
+ if $_ =~ /^[\s]+/
48
+ line << " " << $'
49
+ else
50
+ nextline = $_
51
+
52
+ if line =~ /^\#/
53
+ ds.comments << line
54
+ elsif line =~ /^dn:[\s]*/i
55
+ dn = $'
56
+ ds[dn] = Hash.new {|k,v| k[v] = []}
57
+ elsif line.length == 0
58
+ dn = nil
59
+ elsif line =~ /^([^:]+):([\:]?)[\s]*/
60
+ # $1 is the attribute name
61
+ # $2 is a colon iff the attr-value is base-64 encoded
62
+ # $' is the attr-value
63
+ # Avoid the Base64 class because not all Ruby versions have it.
64
+ attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
65
+ ds[dn][$1.downcase.intern] << attrvalue
66
+ end
67
+
68
+ line = nextline
69
+ end
70
+ end
71
+
72
+ ds
73
+ end
74
+
75
+
76
+ def initialize
77
+ @comments = []
78
+ end
79
+
80
+
81
+ def to_ldif
82
+ ary = []
83
+ ary += (@comments || [])
84
+
85
+ keys.sort.each {|dn|
86
+ ary << "dn: #{dn}"
87
+
88
+ self[dn].keys.map {|sym| sym.to_s}.sort.each {|attr|
89
+ self[dn][attr.intern].each {|val|
90
+ ary << "#{attr}: #{val}"
91
+ }
92
+ }
93
+
94
+ ary << ""
95
+ }
96
+
97
+ block_given? and ary.each {|line| yield line}
98
+
99
+ ary
100
+ end
101
+
102
+
103
+ end # Dataset
104
+
105
+ end # LDAP
106
+ end # Net
107
+
108
+
@@ -0,0 +1,269 @@
1
+ # $Id$
2
+ #
3
+ # LDAP Entry (search-result) support classes
4
+ #
5
+ #
6
+ #----------------------------------------------------------------------------
7
+ #
8
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
9
+ #
10
+ # Gmail: garbagecat10
11
+ #
12
+ # This program is free software; you can redistribute it and/or modify
13
+ # it under the terms of the GNU General Public License as published by
14
+ # the Free Software Foundation; either version 2 of the License, or
15
+ # (at your option) any later version.
16
+ #
17
+ # This program is distributed in the hope that it will be useful,
18
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ # GNU General Public License for more details.
21
+ #
22
+ # You should have received a copy of the GNU General Public License
23
+ # along with this program; if not, write to the Free Software
24
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
25
+ #
26
+ #---------------------------------------------------------------------------
27
+ #
28
+
29
+
30
+ require 'base64'
31
+
32
+
33
+ module Net
34
+ class LDAP
35
+
36
+
37
+ # Objects of this class represent individual entries in an LDAP
38
+ # directory. User code generally does not instantiate this class.
39
+ # Net::LDAP#search provides objects of this class to user code,
40
+ # either as block parameters or as return values.
41
+ #
42
+ # In LDAP-land, an "entry" is a collection of attributes that are
43
+ # uniquely and globally identified by a DN ("Distinguished Name").
44
+ # Attributes are identified by short, descriptive words or phrases.
45
+ # Although a directory is
46
+ # free to implement any attribute name, most of them follow rigorous
47
+ # standards so that the range of commonly-encountered attribute
48
+ # names is not large.
49
+ #
50
+ # An attribute name is case-insensitive. Most directories also
51
+ # restrict the range of characters allowed in attribute names.
52
+ # To simplify handling attribute names, Net::LDAP::Entry
53
+ # internally converts them to a standard format. Therefore, the
54
+ # methods which take attribute names can take Strings or Symbols,
55
+ # and work correctly regardless of case or capitalization.
56
+ #
57
+ # An attribute consists of zero or more data items called
58
+ # <i>values.</i> An entry is the combination of a unique DN, a set of attribute
59
+ # names, and a (possibly-empty) array of values for each attribute.
60
+ #
61
+ # Class Net::LDAP::Entry provides convenience methods for dealing
62
+ # with LDAP entries.
63
+ # In addition to the methods documented below, you may access individual
64
+ # attributes of an entry simply by giving the attribute name as
65
+ # the name of a method call. For example:
66
+ # ldap.search( ... ) do |entry|
67
+ # puts "Common name: #{entry.cn}"
68
+ # puts "Email addresses:"
69
+ # entry.mail.each {|ma| puts ma}
70
+ # end
71
+ # If you use this technique to access an attribute that is not present
72
+ # in a particular Entry object, a NoMethodError exception will be raised.
73
+ #
74
+ #--
75
+ # Ugly problem to fix someday: We key off the internal hash with
76
+ # a canonical form of the attribute name: convert to a string,
77
+ # downcase, then take the symbol. Unfortunately we do this in
78
+ # at least three places. Should do it in ONE place.
79
+ class Entry
80
+
81
+
82
+ # This constructor is not generally called by user code.
83
+ #--
84
+ # Originally, myhash took a block so we wouldn't have to
85
+ # make sure its elements returned empty arrays when necessary.
86
+ # Got rid of that to enable marshalling of Entry objects,
87
+ # but that doesn't work anyway, because Entry objects have
88
+ # singleton methods. So we define a custom dump and load.
89
+ def initialize dn = nil # :nodoc:
90
+ @myhash = {} # originally: Hash.new {|k,v| k[v] = [] }
91
+ @myhash[:dn] = [dn]
92
+ end
93
+
94
+ def _dump depth
95
+ to_ldif
96
+ end
97
+
98
+ class << self
99
+ def _load entry
100
+ from_single_ldif_string entry
101
+ end
102
+ end
103
+
104
+ #--
105
+ # Discovered bug, 26Aug06: I noticed that we're not converting the
106
+ # incoming value to an array if it isn't already one.
107
+ def []= name, value # :nodoc:
108
+ sym = name.to_s.downcase.intern
109
+ value = [value] unless value.is_a?(Array)
110
+ @myhash[sym] = value
111
+ end
112
+
113
+
114
+ #--
115
+ # We have to deal with this one as we do with []=
116
+ # because this one and not the other one gets called
117
+ # in formulations like entry["CN"] << cn.
118
+ #
119
+ def [] name # :nodoc:
120
+ name = name.to_s.downcase.intern unless name.is_a?(Symbol)
121
+ @myhash[name] || []
122
+ end
123
+
124
+ # Returns the dn of the Entry as a String.
125
+ def dn
126
+ self[:dn][0].to_s
127
+ end
128
+
129
+ # Returns an array of the attribute names present in the Entry.
130
+ def attribute_names
131
+ @myhash.keys
132
+ end
133
+
134
+ # Accesses each of the attributes present in the Entry.
135
+ # Calls a user-supplied block with each attribute in turn,
136
+ # passing two arguments to the block: a Symbol giving
137
+ # the name of the attribute, and a (possibly empty)
138
+ # Array of data values.
139
+ #
140
+ def each
141
+ if block_given?
142
+ attribute_names.each {|a|
143
+ attr_name,values = a,self[a]
144
+ yield attr_name, values
145
+ }
146
+ end
147
+ end
148
+
149
+ alias_method :each_attribute, :each
150
+
151
+
152
+
153
+ # Converts the Entry to a String, representing the
154
+ # Entry's attributes in LDIF format.
155
+ #--
156
+ def to_ldif
157
+ ary = []
158
+ ary << "dn: #{dn}\n"
159
+ v2 = "" # temp value, save on GC
160
+ each_attribute do |k,v|
161
+ unless k == :dn
162
+ v.each {|v1|
163
+ v2 = if (k == :userpassword) || is_attribute_value_binary?(v1)
164
+ ": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}"
165
+ else
166
+ " #{v1}"
167
+ end
168
+ ary << "#{k}:#{v2}\n"
169
+ }
170
+ end
171
+ end
172
+ ary << "\n"
173
+ ary.join
174
+ end
175
+
176
+ #--
177
+ # TODO, doesn't support broken lines.
178
+ # It generates a SINGLE Entry object from an incoming LDIF stream
179
+ # which is of course useless for big LDIF streams that encode
180
+ # many objects.
181
+ # DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED.
182
+ # As it is, it's useful for unmarshalling objects that we create,
183
+ # but not for reading arbitrary LDIF files.
184
+ # Eventually, we should have a class method that parses large LDIF
185
+ # streams into individual LDIF blocks (delimited by blank lines)
186
+ # and passes them here.
187
+ #
188
+ # There is one oddity, noticed by Matthias Tarasiewicz: as originally
189
+ # written, this code would return an Entry object in which the DN
190
+ # attribute consisted of a two-element array, and the first element was
191
+ # nil. That's because Entry#initialize doesn't like to create an object
192
+ # without a DN attribute so it adds one: nil. The workaround here is
193
+ # to wipe out the nil DN after creating the Entry object, and trust the
194
+ # LDIF string to fill it in. If it doesn't we return a nil at the end.
195
+ # (30Sep06, FCianfrocca)
196
+ #
197
+ class << self
198
+ def from_single_ldif_string ldif
199
+ entry = Entry.new
200
+ entry[:dn] = []
201
+ ldif.split(/\r?\n/m).each {|line|
202
+ break if line.length == 0
203
+ if line =~ /\A([\w]+):(:?)[\s]*/
204
+ entry[$1] <<= if $2 == ':'
205
+ Base64.decode64($')
206
+ else
207
+ $'
208
+ end
209
+ end
210
+ }
211
+ entry.dn ? entry : nil
212
+ end
213
+ end
214
+
215
+ #--
216
+ # Convenience method to convert unknown method names
217
+ # to attribute references. Of course the method name
218
+ # comes to us as a symbol, so let's save a little time
219
+ # and not bother with the to_s.downcase two-step.
220
+ # Of course that means that a method name like mAIL
221
+ # won't work, but we shouldn't be encouraging that
222
+ # kind of bad behavior in the first place.
223
+ # Maybe we should thow something if the caller sends
224
+ # arguments or a block...
225
+ #
226
+ def method_missing *args, &block # :nodoc:
227
+ s = args[0].to_s.downcase.intern
228
+ if attribute_names.include?(s)
229
+ self[s]
230
+ elsif s.to_s[-1] == 61 and s.to_s.length > 1
231
+ value = args[1] or raise RuntimeError.new( "unable to set value" )
232
+ value = [value] unless value.is_a?(Array)
233
+ name = s.to_s[0..-2].intern
234
+ self[name] = value
235
+ else
236
+ raise NoMethodError.new( "undefined method '#{s}'" )
237
+ end
238
+ end
239
+
240
+ def write
241
+ end
242
+
243
+
244
+ #--
245
+ # Internal convenience method. It seems like the standard
246
+ # approach in most LDAP tools to base64 encode an attribute
247
+ # value if its first or last byte is nonprintable, or if
248
+ # it's a password. But that turns out to be not nearly good
249
+ # enough. There are plenty of A/D attributes that are binary
250
+ # in the middle. This is probably a nasty performance killer.
251
+ def is_attribute_value_binary? value
252
+ v = value.to_s
253
+ v.each_byte {|byt|
254
+ return true if (byt < 32) || (byt > 126)
255
+ }
256
+ if v[0..0] == ':' or v[0..0] == '<'
257
+ return true
258
+ end
259
+ false
260
+ end
261
+ private :is_attribute_value_binary?
262
+
263
+ end # class Entry
264
+
265
+
266
+ end # class LDAP
267
+ end # module Net
268
+
269
+
@@ -0,0 +1,499 @@
1
+ # $Id$
2
+ #
3
+ #
4
+ #----------------------------------------------------------------------------
5
+ #
6
+ # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7
+ #
8
+ # Gmail: garbagecat10
9
+ #
10
+ # This program is free software; you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation; either version 2 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23
+ #
24
+ #---------------------------------------------------------------------------
25
+ #
26
+ #
27
+
28
+
29
+ module Net
30
+ class LDAP
31
+
32
+
33
+ # Class Net::LDAP::Filter is used to constrain
34
+ # LDAP searches. An object of this class is
35
+ # passed to Net::LDAP#search in the parameter :filter.
36
+ #
37
+ # Net::LDAP::Filter supports the complete set of search filters
38
+ # available in LDAP, including conjunction, disjunction and negation
39
+ # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
40
+ # standard notation for specifying LDAP search filters.
41
+ #
42
+ # Here's how to code the familiar "objectclass is present" filter:
43
+ # f = Net::LDAP::Filter.pres( "objectclass" )
44
+ # The object returned by this code can be passed directly to
45
+ # the <tt>:filter</tt> parameter of Net::LDAP#search.
46
+ #
47
+ # See the individual class and instance methods below for more examples.
48
+ #
49
+ class Filter
50
+
51
+ def initialize op, a, b
52
+ @op = op
53
+ @left = a
54
+ @right = b
55
+ end
56
+
57
+ # #eq creates a filter object indicating that the value of
58
+ # a paticular attribute must be either <i>present</i> or must
59
+ # match a particular string.
60
+ #
61
+ # To specify that an attribute is "present" means that only
62
+ # directory entries which contain a value for the particular
63
+ # attribute will be selected by the filter. This is useful
64
+ # in case of optional attributes such as <tt>mail.</tt>
65
+ # Presence is indicated by giving the value "*" in the second
66
+ # parameter to #eq. This example selects only entries that have
67
+ # one or more values for <tt>sAMAccountName:</tt>
68
+ # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
69
+ #
70
+ # To match a particular range of values, pass a string as the
71
+ # second parameter to #eq. The string may contain one or more
72
+ # "*" characters as wildcards: these match zero or more occurrences
73
+ # of any character. Full regular-expressions are <i>not</i> supported
74
+ # due to limitations in the underlying LDAP protocol.
75
+ # This example selects any entry with a <tt>mail</tt> value containing
76
+ # the substring "anderson":
77
+ # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
78
+ #--
79
+ # Removed gt and lt. They ain't in the standard!
80
+ #
81
+ def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
82
+ def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
83
+ #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
84
+ #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
85
+ def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
86
+ def Filter::le attribute, value; Filter.new :le, attribute, value; end
87
+
88
+ # #pres( attribute ) is a synonym for #eq( attribute, "*" )
89
+ #
90
+ def Filter::pres attribute; Filter.eq attribute, "*"; end
91
+
92
+ # operator & ("AND") is used to conjoin two or more filters.
93
+ # This expression will select only entries that have an <tt>objectclass</tt>
94
+ # attribute AND have a <tt>mail</tt> attribute that begins with "George":
95
+ # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
96
+ #
97
+ def & filter; Filter.new :and, self, filter; end
98
+
99
+ # operator | ("OR") is used to disjoin two or more filters.
100
+ # This expression will select entries that have either an <tt>objectclass</tt>
101
+ # attribute OR a <tt>mail</tt> attribute that begins with "George":
102
+ # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
103
+ #
104
+ def | filter; Filter.new :or, self, filter; end
105
+
106
+
107
+ #
108
+ # operator ~ ("NOT") is used to negate a filter.
109
+ # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
110
+ # attribute:
111
+ # f = ~ Net::LDAP::Filter.pres( "objectclass" )
112
+ #
113
+ #--
114
+ # This operator can't be !, evidently. Try it.
115
+ # Removed GT and LT. They're not in the RFC.
116
+ def ~@; Filter.new :not, self, nil; end
117
+
118
+ # Equality operator for filters, useful primarily for constructing unit tests.
119
+ def == filter
120
+ str = "[@op,@left,@right]"
121
+ self.instance_eval(str) == filter.instance_eval(str)
122
+ end
123
+
124
+ def to_s
125
+ case @op
126
+ when :ne
127
+ "(!(#{@left}=#{@right}))"
128
+ when :eq
129
+ "(#{@left}=#{@right})"
130
+ #when :gt
131
+ # "#{@left}>#{@right}"
132
+ #when :lt
133
+ # "#{@left}<#{@right}"
134
+ when :ge
135
+ "#{@left}>=#{@right}"
136
+ when :le
137
+ "#{@left}<=#{@right}"
138
+ when :and
139
+ "(&(#{@left})(#{@right}))"
140
+ when :or
141
+ "(|(#{@left})(#{@right}))"
142
+ when :not
143
+ "(!(#{@left}))"
144
+ else
145
+ raise "invalid or unsupported operator in LDAP Filter"
146
+ end
147
+ end
148
+
149
+
150
+ #--
151
+ # to_ber
152
+ # Filter ::=
153
+ # CHOICE {
154
+ # and [0] SET OF Filter,
155
+ # or [1] SET OF Filter,
156
+ # not [2] Filter,
157
+ # equalityMatch [3] AttributeValueAssertion,
158
+ # substrings [4] SubstringFilter,
159
+ # greaterOrEqual [5] AttributeValueAssertion,
160
+ # lessOrEqual [6] AttributeValueAssertion,
161
+ # present [7] AttributeType,
162
+ # approxMatch [8] AttributeValueAssertion
163
+ # }
164
+ #
165
+ # SubstringFilter
166
+ # SEQUENCE {
167
+ # type AttributeType,
168
+ # SEQUENCE OF CHOICE {
169
+ # initial [0] LDAPString,
170
+ # any [1] LDAPString,
171
+ # final [2] LDAPString
172
+ # }
173
+ # }
174
+ #
175
+ # Parsing substrings is a little tricky.
176
+ # We use the split method to break a string into substrings
177
+ # delimited by the * (star) character. But we also need
178
+ # to know whether there is a star at the head and tail
179
+ # of the string. A Ruby particularity comes into play here:
180
+ # if you split on * and the first character of the string is
181
+ # a star, then split will return an array whose first element
182
+ # is an _empty_ string. But if the _last_ character of the
183
+ # string is star, then split will return an array that does
184
+ # _not_ add an empty string at the end. So we have to deal
185
+ # with all that specifically.
186
+ #
187
+ def to_ber
188
+ case @op
189
+ when :eq
190
+ if @right == "*" # present
191
+ @left.to_s.to_ber_contextspecific 7
192
+ elsif @right =~ /[\*]/ #substring
193
+ ary = @right.split( /[\*]+/ )
194
+ final_star = @right =~ /[\*]$/
195
+ initial_star = ary.first == "" and ary.shift
196
+
197
+ seq = []
198
+ unless initial_star
199
+ seq << ary.shift.to_ber_contextspecific(0)
200
+ end
201
+ n_any_strings = ary.length - (final_star ? 0 : 1)
202
+ #p n_any_strings
203
+ n_any_strings.times {
204
+ seq << ary.shift.to_ber_contextspecific(1)
205
+ }
206
+ unless final_star
207
+ seq << ary.shift.to_ber_contextspecific(2)
208
+ end
209
+ [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
210
+ else #equality
211
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 3
212
+ end
213
+ when :ge
214
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 5
215
+ when :le
216
+ [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific 6
217
+ when :and
218
+ ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
219
+ ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
220
+ when :or
221
+ ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
222
+ ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
223
+ when :not
224
+ [@left.to_ber].to_ber_contextspecific 2
225
+ else
226
+ # ERROR, we'll return objectclass=* to keep things from blowing up,
227
+ # but that ain't a good answer and we need to kick out an error of some kind.
228
+ raise "unimplemented search filter"
229
+ end
230
+ end
231
+
232
+ def unescape(right)
233
+ right.gsub(/\\([a-fA-F\d]{2,2})/) do
234
+ [$1.hex].pack("U")
235
+ end
236
+ end
237
+
238
+
239
+ # Converts an LDAP search filter in BER format to an Net::LDAP::Filter
240
+ # object. The incoming BER object most likely came to us by parsing an
241
+ # LDAP searchRequest PDU.
242
+ # Cf the comments under #to_ber, including the grammar snippet from the RFC.
243
+ #--
244
+ # We're hardcoding the BER constants from the RFC. Ought to break them out
245
+ # into constants.
246
+ #
247
+ def Filter::parse_ber ber
248
+ case ber.ber_identifier
249
+ when 0xa0 # context-specific constructed 0, "and"
250
+ ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo & obj}
251
+ when 0xa1 # context-specific constructed 1, "or"
252
+ ber.map {|b| Filter::parse_ber(b)}.inject {|memo,obj| memo | obj}
253
+ when 0xa2 # context-specific constructed 2, "not"
254
+ ~ Filter::parse_ber( ber.first )
255
+ when 0xa3 # context-specific constructed 3, "equalityMatch"
256
+ if ber.last == "*"
257
+ else
258
+ Filter.eq( ber.first, ber.last )
259
+ end
260
+ when 0xa4 # context-specific constructed 4, "substring"
261
+ str = ""
262
+ final = false
263
+ ber.last.each {|b|
264
+ case b.ber_identifier
265
+ when 0x80 # context-specific primitive 0, SubstringFilter "initial"
266
+ raise "unrecognized substring filter, bad initial" if str.length > 0
267
+ str += b
268
+ when 0x81 # context-specific primitive 0, SubstringFilter "any"
269
+ str += "*#{b}"
270
+ when 0x82 # context-specific primitive 0, SubstringFilter "final"
271
+ str += "*#{b}"
272
+ final = true
273
+ end
274
+ }
275
+ str += "*" unless final
276
+ Filter.eq( ber.first.to_s, str )
277
+ when 0xa5 # context-specific constructed 5, "greaterOrEqual"
278
+ Filter.ge( ber.first.to_s, ber.last.to_s )
279
+ when 0xa6 # context-specific constructed 5, "lessOrEqual"
280
+ Filter.le( ber.first.to_s, ber.last.to_s )
281
+ when 0x87 # context-specific primitive 7, "present"
282
+ # call to_s to get rid of the BER-identifiedness of the incoming string.
283
+ Filter.pres( ber.to_s )
284
+ else
285
+ raise "invalid BER tag-value (#{ber.ber_identifier}) in search filter"
286
+ end
287
+ end
288
+
289
+
290
+ # Perform filter operations against a user-supplied block. This is useful when implementing
291
+ # an LDAP directory server. The caller's block will be called with two arguments: first, a
292
+ # symbol denoting the "operation" of the filter; and second, an array consisting of arguments
293
+ # to the operation. The user-supplied block (which is MANDATORY) should perform some desired
294
+ # application-defined processing, and may return a locally-meaningful object that will appear
295
+ # as a parameter in the :and, :or and :not operations detailed below.
296
+ #
297
+ # A typical object to return from the user-supplied block is an array of
298
+ # Net::LDAP::Filter objects.
299
+ #
300
+ # These are the possible values that may be passed to the user-supplied block:
301
+ # :equalityMatch (the arguments will be an attribute name and a value to be matched);
302
+ # :substrings (two arguments: an attribute name and a value containing one or more * characters);
303
+ # :present (one argument: an attribute name);
304
+ # :greaterOrEqual (two arguments: an attribute name and a value to be compared against);
305
+ # :lessOrEqual (two arguments: an attribute name and a value to be compared against);
306
+ # :and (two or more arguments, each of which is an object returned from a recursive call
307
+ # to #execute, with the same block;
308
+ # :or (two or more arguments, each of which is an object returned from a recursive call
309
+ # to #execute, with the same block;
310
+ # :not (one argument, which is an object returned from a recursive call to #execute with the
311
+ # the same block.
312
+ #
313
+ def execute &block
314
+ case @op
315
+ when :eq
316
+ if @right == "*"
317
+ yield :present, @left
318
+ elsif @right.index '*'
319
+ yield :substrings, @left, @right
320
+ else
321
+ yield :equalityMatch, @left, @right
322
+ end
323
+ when :ge
324
+ yield :greaterOrEqual, @left, @right
325
+ when :le
326
+ yield :lessOrEqual, @left, @right
327
+ when :or, :and
328
+ yield @op, (@left.execute(&block)), (@right.execute(&block))
329
+ when :not
330
+ yield @op, (@left.execute(&block))
331
+ end || []
332
+ end
333
+
334
+
335
+ #--
336
+ # coalesce
337
+ # This is a private helper method for dealing with chains of ANDs and ORs
338
+ # that are longer than two. If BOTH of our branches are of the specified
339
+ # type of joining operator, then return both of them as an array (calling
340
+ # coalesce recursively). If they're not, then return an array consisting
341
+ # only of self.
342
+ #
343
+ def coalesce operator
344
+ if @op == operator
345
+ [@left.coalesce( operator ), @right.coalesce( operator )]
346
+ else
347
+ [self]
348
+ end
349
+ end
350
+
351
+
352
+
353
+ #--
354
+ # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
355
+ # object. Convert it to a Net::LDAP::Filter.
356
+ # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
357
+ # filter types. Could pull them out into a constant.
358
+ #
359
+ def Filter::parse_ldap_filter obj
360
+ case obj.ber_identifier
361
+ when 0x87 # present. context-specific primitive 7.
362
+ Filter.eq( obj.to_s, "*" )
363
+ when 0xa3 # equalityMatch. context-specific constructed 3.
364
+ Filter.eq( obj[0], obj[1] )
365
+ else
366
+ raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
367
+ end
368
+ end
369
+
370
+
371
+
372
+
373
+ #--
374
+ # We got a hash of attribute values.
375
+ # Do we match the attributes?
376
+ # Return T/F, and call match recursively as necessary.
377
+ def match entry
378
+ case @op
379
+ when :eq
380
+ if @right == "*"
381
+ l = entry[@left] and l.length > 0
382
+ else
383
+ l = entry[@left] and l = l.to_a and l.index(@right)
384
+ end
385
+ else
386
+ raise LdapError.new( "unknown filter type in match: #{@op}" )
387
+ end
388
+ end
389
+
390
+ # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
391
+ # to a Net::LDAP::Filter.
392
+ def self.construct ldap_filter_string
393
+ FilterParser.new(ldap_filter_string).filter
394
+ end
395
+
396
+ # Synonym for #construct.
397
+ # to a Net::LDAP::Filter.
398
+ def self.from_rfc2254 ldap_filter_string
399
+ construct ldap_filter_string
400
+ end
401
+
402
+ end # class Net::LDAP::Filter
403
+
404
+
405
+
406
+ class FilterParser #:nodoc:
407
+
408
+ attr_reader :filter
409
+
410
+ def initialize str
411
+ require 'strscan'
412
+ @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
413
+ end
414
+
415
+ def parse scanner
416
+ parse_filter_branch(scanner) or parse_paren_expression(scanner)
417
+ end
418
+
419
+ def parse_paren_expression scanner
420
+ if scanner.scan(/\s*\(\s*/)
421
+ b = if scanner.scan(/\s*\&\s*/)
422
+ a = nil
423
+ branches = []
424
+ while br = parse_paren_expression(scanner)
425
+ branches << br
426
+ end
427
+ if branches.length >= 2
428
+ a = branches.shift
429
+ while branches.length > 0
430
+ a = a & branches.shift
431
+ end
432
+ a
433
+ end
434
+ elsif scanner.scan(/\s*\|\s*/)
435
+ # TODO: DRY!
436
+ a = nil
437
+ branches = []
438
+ while br = parse_paren_expression(scanner)
439
+ branches << br
440
+ end
441
+ if branches.length >= 2
442
+ a = branches.shift
443
+ while branches.length > 0
444
+ a = a | branches.shift
445
+ end
446
+ a
447
+ end
448
+ elsif scanner.scan(/\s*\!\s*/)
449
+ br = parse_paren_expression(scanner)
450
+ if br
451
+ ~ br
452
+ end
453
+ else
454
+ parse_filter_branch( scanner )
455
+ end
456
+
457
+ if b and scanner.scan( /\s*\)\s*/ )
458
+ b
459
+ end
460
+ end
461
+ end
462
+
463
+ # Added a greatly-augmented filter contributed by Andre Nathan
464
+ # for detecting special characters in values. (15Aug06)
465
+ # Added blanks to the attribute filter (26Oct06)
466
+ def parse_filter_branch scanner
467
+ scanner.scan(/\s*/)
468
+ if token = scanner.scan( /[\w\-_]+/ )
469
+ scanner.scan(/\s*/)
470
+ if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
471
+ scanner.scan(/\s*/)
472
+ #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
473
+ #if value = scanner.scan( /[\w\*\.\+\-@=#\$%&! ]+/ ) (ff suggested by Kouhei Sutou
474
+ if value = scanner.scan( /(?:[\w\*\.\+\-@=,#\$%&! ]|\\[a-fA-F\d]{2,2})+/ )
475
+ case op
476
+ when "="
477
+ Filter.eq( token, value )
478
+ when "!="
479
+ Filter.ne( token, value )
480
+ when "<"
481
+ Filter.lt( token, value )
482
+ when "<="
483
+ Filter.le( token, value )
484
+ when ">"
485
+ Filter.gt( token, value )
486
+ when ">="
487
+ Filter.ge( token, value )
488
+ end
489
+ end
490
+ end
491
+ end
492
+ end
493
+
494
+ end # class Net::LDAP::FilterParser
495
+
496
+ end # class Net::LDAP
497
+ end # module Net
498
+
499
+