net-ldap 0.3.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/Contributors.rdoc +4 -0
  3. data/Hacking.rdoc +3 -8
  4. data/History.rdoc +181 -0
  5. data/README.rdoc +44 -12
  6. data/lib/net-ldap.rb +1 -1
  7. data/lib/net/ber.rb +41 -7
  8. data/lib/net/ber/ber_parser.rb +21 -7
  9. data/lib/net/ber/core_ext.rb +11 -18
  10. data/lib/net/ber/core_ext/array.rb +14 -0
  11. data/lib/net/ber/core_ext/integer.rb +74 -0
  12. data/lib/net/ber/core_ext/string.rb +24 -4
  13. data/lib/net/ber/core_ext/true_class.rb +2 -3
  14. data/lib/net/ldap.rb +441 -639
  15. data/lib/net/ldap/auth_adapter.rb +29 -0
  16. data/lib/net/ldap/auth_adapter/gss_spnego.rb +41 -0
  17. data/lib/net/ldap/auth_adapter/sasl.rb +62 -0
  18. data/lib/net/ldap/auth_adapter/simple.rb +34 -0
  19. data/lib/net/ldap/connection.rb +716 -0
  20. data/lib/net/ldap/dataset.rb +23 -9
  21. data/lib/net/ldap/dn.rb +13 -14
  22. data/lib/net/ldap/entry.rb +27 -9
  23. data/lib/net/ldap/error.rb +49 -0
  24. data/lib/net/ldap/filter.rb +58 -32
  25. data/lib/net/ldap/instrumentation.rb +23 -0
  26. data/lib/net/ldap/password.rb +23 -14
  27. data/lib/net/ldap/pdu.rb +70 -6
  28. data/lib/net/ldap/version.rb +5 -0
  29. data/lib/net/snmp.rb +237 -241
  30. metadata +71 -116
  31. data/.autotest +0 -11
  32. data/.gemtest +0 -0
  33. data/.rspec +0 -2
  34. data/Manifest.txt +0 -49
  35. data/Rakefile +0 -74
  36. data/autotest/discover.rb +0 -1
  37. data/lib/net/ber/core_ext/bignum.rb +0 -22
  38. data/lib/net/ber/core_ext/fixnum.rb +0 -66
  39. data/net-ldap.gemspec +0 -58
  40. data/spec/integration/ssl_ber_spec.rb +0 -36
  41. data/spec/spec.opts +0 -2
  42. data/spec/spec_helper.rb +0 -5
  43. data/spec/unit/ber/ber_spec.rb +0 -109
  44. data/spec/unit/ber/core_ext/string_spec.rb +0 -51
  45. data/spec/unit/ldap/dn_spec.rb +0 -80
  46. data/spec/unit/ldap/entry_spec.rb +0 -51
  47. data/spec/unit/ldap/filter_spec.rb +0 -84
  48. data/spec/unit/ldap_spec.rb +0 -48
  49. data/test/common.rb +0 -3
  50. data/test/test_entry.rb +0 -59
  51. data/test/test_filter.rb +0 -122
  52. data/test/test_ldap_connection.rb +0 -24
  53. data/test/test_ldif.rb +0 -79
  54. data/test/test_password.rb +0 -17
  55. data/test/test_rename.rb +0 -77
  56. data/test/test_snmp.rb +0 -114
  57. data/test/testdata.ldif +0 -101
  58. data/testserver/ldapserver.rb +0 -210
  59. data/testserver/testdata.ldif +0 -101
@@ -0,0 +1,29 @@
1
+ module Net
2
+ class LDAP
3
+ class AuthAdapter
4
+ def self.register(names, adapter)
5
+ names = Array(names)
6
+ @adapters ||= {}
7
+ names.each do |name|
8
+ @adapters[name] = adapter
9
+ end
10
+ end
11
+
12
+ def self.[](name)
13
+ a = @adapters[name]
14
+ if a.nil?
15
+ raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})"
16
+ end
17
+ return a
18
+ end
19
+
20
+ def initialize(conn)
21
+ @connection = conn
22
+ end
23
+
24
+ def bind
25
+ raise "bind method must be overwritten"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../auth_adapter'
2
+ require_relative 'sasl'
3
+
4
+ module Net
5
+ class LDAP
6
+ module AuthAdapers
7
+ #--
8
+ # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
9
+ # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
10
+ # integrate it without introducing an external dependency.
11
+ #
12
+ # This authentication method is accessed by calling #bind with a :method
13
+ # parameter of :gss_spnego. It requires :username and :password
14
+ # attributes, just like the :simple authentication method. It performs a
15
+ # GSS-SPNEGO authentication with the server, which is presumed to be a
16
+ # Microsoft Active Directory.
17
+ #++
18
+ class GSS_SPNEGO < Net::LDAP::AuthAdapter
19
+ def bind(auth)
20
+ require 'ntlm'
21
+
22
+ user, psw = [auth[:username] || auth[:dn], auth[:password]]
23
+ raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
24
+
25
+ nego = proc do |challenge|
26
+ t2_msg = NTLM::Message.parse(challenge)
27
+ t3_msg = t2_msg.response({ :user => user, :password => psw },
28
+ { :ntlmv2 => true })
29
+ t3_msg.serialize
30
+ end
31
+
32
+ Net::LDAP::AuthAdapter::Sasl.new(@connection).bind \
33
+ :method => :sasl,
34
+ :mechanism => "GSS-SPNEGO",
35
+ :initial_credential => NTLM::Message::Type1.new.serialize,
36
+ :challenge_response => nego
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ require_relative '../auth_adapter'
2
+
3
+ module Net
4
+ class LDAP
5
+ class AuthAdapter
6
+ class Sasl < Net::LDAP::AuthAdapter
7
+ MAX_SASL_CHALLENGES = 10
8
+
9
+ #--
10
+ # Required parameters: :mechanism, :initial_credential and
11
+ # :challenge_response
12
+ #
13
+ # Mechanism is a string value that will be passed in the SASL-packet's
14
+ # "mechanism" field.
15
+ #
16
+ # Initial credential is most likely a string. It's passed in the initial
17
+ # BindRequest that goes to the server. In some protocols, it may be empty.
18
+ #
19
+ # Challenge-response is a Ruby proc that takes a single parameter and
20
+ # returns an object that will typically be a string. The
21
+ # challenge-response block is called when the server returns a
22
+ # BindResponse with a result code of 14 (saslBindInProgress). The
23
+ # challenge-response block receives a parameter containing the data
24
+ # returned by the server in the saslServerCreds field of the LDAP
25
+ # BindResponse packet. The challenge-response block may be called multiple
26
+ # times during the course of a SASL authentication, and each time it must
27
+ # return a value that will be passed back to the server as the credential
28
+ # data in the next BindRequest packet.
29
+ #++
30
+ def bind(auth)
31
+ mech, cred, chall = auth[:mechanism], auth[:initial_credential],
32
+ auth[:challenge_response]
33
+ raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
34
+
35
+ message_id = @connection.next_msgid
36
+
37
+ n = 0
38
+ loop do
39
+ sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
40
+ request = [
41
+ Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl
42
+ ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
43
+
44
+ @connection.send(:write, request, nil, message_id)
45
+ pdu = @connection.queued_read(message_id)
46
+
47
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
48
+ raise Net::LDAP::NoBindResultError, "no bind result"
49
+ end
50
+
51
+ return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
52
+ raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MAX_SASL_CHALLENGES)
53
+
54
+ cred = chall.call(pdu.result_server_sasl_creds)
55
+ end
56
+
57
+ raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../auth_adapter'
2
+
3
+ module Net
4
+ class LDAP
5
+ class AuthAdapter
6
+ class Simple < AuthAdapter
7
+ def bind(auth)
8
+ user, psw = if auth[:method] == :simple
9
+ [auth[:username] || auth[:dn], auth[:password]]
10
+ else
11
+ ["", ""]
12
+ end
13
+
14
+ raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
15
+
16
+ message_id = @connection.next_msgid
17
+ request = [
18
+ Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber,
19
+ psw.to_ber_contextspecific(0)
20
+ ].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
21
+
22
+ @connection.send(:write, request, nil, message_id)
23
+ pdu = @connection.queued_read(message_id)
24
+
25
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
26
+ raise Net::LDAP::NoBindResultError, "no bind result"
27
+ end
28
+
29
+ pdu
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,716 @@
1
+ # This is a private class used internally by the library. It should not
2
+ # be called by user code.
3
+ class Net::LDAP::Connection #:nodoc:
4
+ include Net::LDAP::Instrumentation
5
+
6
+ # Seconds before failing for socket connect timeout
7
+ DefaultConnectTimeout = 5
8
+
9
+ LdapVersion = 3
10
+
11
+ # Initialize a connection to an LDAP server
12
+ #
13
+ # :server
14
+ # :hosts Array of tuples specifying host, port
15
+ # :host host
16
+ # :port port
17
+ # :socket prepared socket
18
+ #
19
+ def initialize(server = {})
20
+ @server = server
21
+ @instrumentation_service = server[:instrumentation_service]
22
+
23
+ # Allows tests to parameterize what socket class to use
24
+ @socket_class = server.fetch(:socket_class, DefaultSocket)
25
+
26
+ yield self if block_given?
27
+ end
28
+
29
+ def socket_class=(socket_class)
30
+ @socket_class = socket_class
31
+ end
32
+
33
+ def prepare_socket(server, timeout=nil)
34
+ socket = server[:socket]
35
+ encryption = server[:encryption]
36
+
37
+ @conn = socket
38
+ setup_encryption(encryption, timeout) if encryption
39
+ end
40
+
41
+ def open_connection(server)
42
+ hosts = server[:hosts]
43
+ encryption = server[:encryption]
44
+
45
+ timeout = server[:connect_timeout] || DefaultConnectTimeout
46
+ socket_opts = {
47
+ connect_timeout: timeout,
48
+ }
49
+
50
+ errors = []
51
+ hosts.each do |host, port|
52
+ begin
53
+ prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts)), timeout)
54
+ if encryption
55
+ if encryption[:tls_options] &&
56
+ encryption[:tls_options][:verify_mode] &&
57
+ encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE
58
+ warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'"
59
+ else
60
+ @conn.post_connection_check(host)
61
+ end
62
+ end
63
+ return
64
+ rescue Net::LDAP::Error, SocketError, SystemCallError,
65
+ OpenSSL::SSL::SSLError => e
66
+ # Ensure the connection is closed in the event a setup failure.
67
+ close
68
+ errors << [e, host, port]
69
+ end
70
+ end
71
+
72
+ raise Net::LDAP::ConnectionError.new(errors)
73
+ end
74
+
75
+ module GetbyteForSSLSocket
76
+ def getbyte
77
+ getc.ord
78
+ end
79
+ end
80
+
81
+ module FixSSLSocketSyncClose
82
+ def close
83
+ super
84
+ io.close
85
+ end
86
+ end
87
+
88
+ def self.wrap_with_ssl(io, tls_options = {}, timeout=nil)
89
+ raise Net::LDAP::NoOpenSSLError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
90
+
91
+ ctx = OpenSSL::SSL::SSLContext.new
92
+
93
+ # By default, we do not verify certificates. For a 1.0 release, this should probably be changed at some point.
94
+ # See discussion in https://github.com/ruby-ldap/ruby-net-ldap/pull/161
95
+ ctx.set_params(tls_options) unless tls_options.empty?
96
+
97
+ conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
98
+
99
+ begin
100
+ if timeout
101
+ conn.connect_nonblock
102
+ else
103
+ conn.connect
104
+ end
105
+ rescue IO::WaitReadable
106
+ raise Errno::ETIMEDOUT, "OpenSSL connection read timeout" unless
107
+ IO.select([conn], nil, nil, timeout)
108
+ retry
109
+ rescue IO::WaitWritable
110
+ raise Errno::ETIMEDOUT, "OpenSSL connection write timeout" unless
111
+ IO.select(nil, [conn], nil, timeout)
112
+ retry
113
+ end
114
+
115
+ # Doesn't work:
116
+ # conn.sync_close = true
117
+
118
+ conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
119
+ conn.extend(FixSSLSocketSyncClose)
120
+
121
+ conn
122
+ end
123
+
124
+ #--
125
+ # Helper method called only from prepare_socket or open_connection, and only
126
+ # after we have a successfully-opened @conn instance variable, which is a TCP
127
+ # connection. Depending on the received arguments, we establish SSL,
128
+ # potentially replacing the value of @conn accordingly. Don't generate any
129
+ # errors here if no encryption is requested. DO raise Net::LDAP::Error objects
130
+ # if encryption is requested and we have trouble setting it up. That includes
131
+ # if OpenSSL is not set up on the machine. (Question: how does the Ruby
132
+ # OpenSSL wrapper react in that case?) DO NOT filter exceptions raised by the
133
+ # OpenSSL library. Let them pass back to the user. That should make it easier
134
+ # for us to debug the problem reports. Presumably (hopefully?) that will also
135
+ # produce recognizable errors if someone tries to use this on a machine
136
+ # without OpenSSL.
137
+ #
138
+ # The simple_tls method is intended as the simplest, stupidest, easiest
139
+ # solution for people who want nothing more than encrypted comms with the
140
+ # LDAP server. It doesn't do any server-cert validation and requires
141
+ # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
142
+ # WE REPLACE the value of @conn, which is presumed to be a connected
143
+ # TCPSocket object.
144
+ #
145
+ # The start_tls method is supported by many servers over the standard LDAP
146
+ # port. It does not require an alternative port for encrypted
147
+ # communications, as with simple_tls. Thanks for Kouhei Sutou for
148
+ # generously contributing the :start_tls path.
149
+ #++
150
+ def setup_encryption(args, timeout=nil)
151
+ args[:tls_options] ||= {}
152
+ case args[:method]
153
+ when :simple_tls
154
+ @conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout)
155
+ # additional branches requiring server validation and peer certs, etc.
156
+ # go here.
157
+ when :start_tls
158
+ message_id = next_msgid
159
+ request = [
160
+ Net::LDAP::StartTlsOid.to_ber_contextspecific(0),
161
+ ].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
162
+
163
+ write(request, nil, message_id)
164
+ pdu = queued_read(message_id)
165
+
166
+ if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
167
+ raise Net::LDAP::NoStartTLSResultError, "no start_tls result"
168
+ end
169
+
170
+ raise Net::LDAP::StartTLSError,
171
+ "start_tls failed: #{pdu.result_code}" unless pdu.result_code.zero?
172
+ @conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout)
173
+ else
174
+ raise Net::LDAP::EncMethodUnsupportedError, "unsupported encryption method #{args[:method]}"
175
+ end
176
+ end
177
+
178
+ #--
179
+ # This is provided as a convenience method to make sure a connection
180
+ # object gets closed without waiting for a GC to happen. Clients shouldn't
181
+ # have to call it, but perhaps it will come in handy someday.
182
+ #++
183
+ def close
184
+ return if !defined?(@conn) || @conn.nil?
185
+ @conn.close
186
+ @conn = nil
187
+ end
188
+
189
+ # Internal: Reads messages by ID from a queue, falling back to reading from
190
+ # the connected socket until a message matching the ID is read. Any messages
191
+ # with mismatched IDs gets queued for subsequent reads by the origin of that
192
+ # message ID.
193
+ #
194
+ # Returns a Net::LDAP::PDU object or nil.
195
+ def queued_read(message_id)
196
+ if pdu = message_queue[message_id].shift
197
+ return pdu
198
+ end
199
+
200
+ # read messages until we have a match for the given message_id
201
+ while pdu = read
202
+ return pdu if pdu.message_id == message_id
203
+
204
+ message_queue[pdu.message_id].push pdu
205
+ next
206
+ end
207
+
208
+ pdu
209
+ end
210
+
211
+ # Internal: The internal queue of messages, read from the socket, grouped by
212
+ # message ID.
213
+ #
214
+ # Used by `queued_read` to return messages sent by the server with the given
215
+ # ID. If no messages are queued for that ID, `queued_read` will `read` from
216
+ # the socket and queue messages that don't match the given ID for other
217
+ # readers.
218
+ #
219
+ # Returns the message queue Hash.
220
+ def message_queue
221
+ @message_queue ||= Hash.new do |hash, key|
222
+ hash[key] = []
223
+ end
224
+ end
225
+
226
+ # Internal: Reads and parses data from the configured connection.
227
+ #
228
+ # - syntax: the BER syntax to use to parse the read data with
229
+ #
230
+ # Returns parsed Net::LDAP::PDU object.
231
+ def read(syntax = Net::LDAP::AsnSyntax)
232
+ ber_object =
233
+ instrument "read.net_ldap_connection", :syntax => syntax do |payload|
234
+ socket.read_ber(syntax) do |id, content_length|
235
+ payload[:object_type_id] = id
236
+ payload[:content_length] = content_length
237
+ end
238
+ end
239
+
240
+ return unless ber_object
241
+
242
+ instrument "parse_pdu.net_ldap_connection" do |payload|
243
+ pdu = payload[:pdu] = Net::LDAP::PDU.new(ber_object)
244
+
245
+ payload[:message_id] = pdu.message_id
246
+ payload[:app_tag] = pdu.app_tag
247
+
248
+ pdu
249
+ end
250
+ end
251
+ private :read
252
+
253
+ # Internal: Write a BER formatted packet with the next message id to the
254
+ # configured connection.
255
+ #
256
+ # - request: required BER formatted request
257
+ # - controls: optional BER formatted controls
258
+ #
259
+ # Returns the return value from writing to the connection, which in some
260
+ # cases is the Integer number of bytes written to the socket.
261
+ def write(request, controls = nil, message_id = next_msgid)
262
+ instrument "write.net_ldap_connection" do |payload|
263
+ packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
264
+ payload[:content_length] = socket.write(packet)
265
+ end
266
+ end
267
+ private :write
268
+
269
+ def next_msgid
270
+ @msgid ||= 0
271
+ @msgid += 1
272
+ end
273
+
274
+ def bind(auth)
275
+ instrument "bind.net_ldap_connection" do |payload|
276
+ payload[:method] = meth = auth[:method]
277
+ adapter = Net::LDAP::AuthAdapter[meth]
278
+ adapter.new(self).bind(auth)
279
+ end
280
+ end
281
+
282
+ #--
283
+ # Allow the caller to specify a sort control
284
+ #
285
+ # The format of the sort control needs to be:
286
+ #
287
+ # :sort_control => ["cn"] # just a string
288
+ # or
289
+ # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
290
+ # or
291
+ # :sort_control => ["givenname","sn"] #multiple strings or arrays
292
+ #
293
+ def encode_sort_controls(sort_definitions)
294
+ return sort_definitions unless sort_definitions
295
+
296
+ sort_control_values = sort_definitions.map do |control|
297
+ control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
298
+ control[0] = String(control[0]).to_ber,
299
+ control[1] = String(control[1]).to_ber,
300
+ control[2] = (control[2] == true).to_ber
301
+ control.to_ber_sequence
302
+ end
303
+ [
304
+ Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
305
+ false.to_ber,
306
+ sort_control_values.to_ber_sequence.to_s.to_ber,
307
+ ].to_ber_sequence
308
+ end
309
+
310
+ #--
311
+ # Alternate implementation, this yields each search entry to the caller as
312
+ # it are received.
313
+ #
314
+ # TODO: certain search parameters are hardcoded.
315
+ # TODO: if we mis-parse the server results or the results are wrong, we
316
+ # can block forever. That's because we keep reading results until we get a
317
+ # type-5 packet, which might never come. We need to support the time-limit
318
+ # in the protocol.
319
+ #++
320
+ def search(args = nil)
321
+ args ||= {}
322
+
323
+ # filtering, scoping, search base
324
+ # filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
325
+ # base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
326
+ # scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
327
+ filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
328
+ base = args[:base]
329
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
330
+
331
+ # attr handling
332
+ # attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
333
+ # attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
334
+ attrs = Array(args[:attributes])
335
+ attrs_only = args[:attributes_only] == true
336
+
337
+ # references
338
+ # refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
339
+ # deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
340
+ refs = args[:return_referrals] == true
341
+ deref = args[:deref] || Net::LDAP::DerefAliases_Never
342
+
343
+ # limiting, paging, sorting
344
+ # size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
345
+ # time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
346
+ size = args[:size].to_i
347
+ time = args[:time].to_i
348
+ paged = args[:paged_searches_supported]
349
+ sort = args.fetch(:sort_controls, false)
350
+
351
+ # arg validation
352
+ raise ArgumentError, "search base is required" unless base
353
+ raise ArgumentError, "invalid search-size" unless size >= 0
354
+ raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
355
+ raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
356
+
357
+ # arg transforms
358
+ filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
359
+ ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
360
+ ber_sort = encode_sort_controls(sort)
361
+
362
+ # An interesting value for the size limit would be close to A/D's
363
+ # built-in page limit of 1000 records, but openLDAP newer than version
364
+ # 2.2.0 chokes on anything bigger than 126. You get a silent error that
365
+ # is easily visible by running slapd in debug mode. Go figure.
366
+ #
367
+ # Changed this around 06Sep06 to support a caller-specified search-size
368
+ # limit. Because we ALWAYS do paged searches, we have to work around the
369
+ # problem that it's not legal to specify a "normal" sizelimit (in the
370
+ # body of the search request) that is larger than the page size we're
371
+ # requesting. Unfortunately, I have the feeling that this will break
372
+ # with LDAP servers that don't support paged searches!!!
373
+ #
374
+ # (Because we pass zero as the sizelimit on search rounds when the
375
+ # remaining limit is larger than our max page size of 126. In these
376
+ # cases, I think the caller's search limit will be ignored!)
377
+ #
378
+ # CONFIRMED: This code doesn't work on LDAPs that don't support paged
379
+ # searches when the size limit is larger than 126. We're going to have
380
+ # to do a root-DSE record search and not do a paged search if the LDAP
381
+ # doesn't support it. Yuck.
382
+ rfc2696_cookie = [126, ""]
383
+ result_pdu = nil
384
+ n_results = 0
385
+
386
+ message_id = next_msgid
387
+
388
+ instrument "search.net_ldap_connection",
389
+ message_id: message_id,
390
+ filter: filter,
391
+ base: base,
392
+ scope: scope,
393
+ size: size,
394
+ time: time,
395
+ sort: sort,
396
+ referrals: refs,
397
+ deref: deref,
398
+ attributes: attrs do |payload|
399
+ loop do
400
+ # should collect this into a private helper to clarify the structure
401
+ query_limit = 0
402
+ if size > 0
403
+ query_limit = if paged
404
+ (((size - n_results) < 126) ? (size - n_results) : 0)
405
+ else
406
+ size
407
+ end
408
+ end
409
+
410
+ request = [
411
+ base.to_ber,
412
+ scope.to_ber_enumerated,
413
+ deref.to_ber_enumerated,
414
+ query_limit.to_ber, # size limit
415
+ time.to_ber,
416
+ attrs_only.to_ber,
417
+ filter.to_ber,
418
+ ber_attrs.to_ber_sequence,
419
+ ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
420
+
421
+ # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
422
+ # this breaks when calling to_ber. (Can't force binary data to UTF-8)
423
+ # we have to disable paging (even though server supports it) to get around this...
424
+
425
+ controls = []
426
+ controls <<
427
+ [
428
+ Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
429
+ # Criticality MUST be false to interoperate with normal LDAPs.
430
+ false.to_ber,
431
+ rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber,
432
+ ].to_ber_sequence if paged
433
+ controls << ber_sort if ber_sort
434
+ controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
435
+
436
+ write(request, controls, message_id)
437
+
438
+ result_pdu = nil
439
+ controls = []
440
+
441
+ while pdu = queued_read(message_id)
442
+ case pdu.app_tag
443
+ when Net::LDAP::PDU::SearchReturnedData
444
+ n_results += 1
445
+ yield pdu.search_entry if block_given?
446
+ when Net::LDAP::PDU::SearchResultReferral
447
+ if refs
448
+ if block_given?
449
+ se = Net::LDAP::Entry.new
450
+ se[:search_referrals] = (pdu.search_referrals || [])
451
+ yield se
452
+ end
453
+ end
454
+ when Net::LDAP::PDU::SearchResult
455
+ result_pdu = pdu
456
+ controls = pdu.result_controls
457
+ if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
458
+ if block_given?
459
+ se = Net::LDAP::Entry.new
460
+ se[:search_referrals] = (pdu.search_referrals || [])
461
+ yield se
462
+ end
463
+ end
464
+ break
465
+ else
466
+ raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
467
+ end
468
+ end
469
+
470
+ if result_pdu.nil?
471
+ raise Net::LDAP::ResponseMissingOrInvalidError, "response missing"
472
+ end
473
+
474
+ # count number of pages of results
475
+ payload[:page_count] ||= 0
476
+ payload[:page_count] += 1
477
+
478
+ # When we get here, we have seen a type-5 response. If there is no
479
+ # error AND there is an RFC-2696 cookie, then query again for the next
480
+ # page of results. If not, we're done. Don't screw this up or we'll
481
+ # break every search we do.
482
+ #
483
+ # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
484
+ # that have a parameter of AsnSyntax? Does this just accidentally
485
+ # work? According to RFC-2696, the value expected in this position is
486
+ # of type OCTET STRING, covered in the default syntax supported by
487
+ # read_ber, so I guess we're ok.
488
+ more_pages = false
489
+ if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
490
+ controls.each do |c|
491
+ if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
492
+ # just in case some bogus server sends us more than 1 of these.
493
+ more_pages = false
494
+ if c.value and c.value.length > 0
495
+ cookie = c.value.read_ber[1]
496
+ if cookie and cookie.length > 0
497
+ rfc2696_cookie[1] = cookie
498
+ more_pages = true
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end
504
+
505
+ break unless more_pages
506
+ end # loop
507
+
508
+ # track total result count
509
+ payload[:result_count] = n_results
510
+
511
+ result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
512
+ end # instrument
513
+ ensure
514
+
515
+ # clean up message queue for this search
516
+ messages = message_queue.delete(message_id)
517
+
518
+ # in the exceptional case some messages were *not* consumed from the queue,
519
+ # instrument the event but do not fail.
520
+ if !messages.nil? && !messages.empty?
521
+ instrument "search_messages_unread.net_ldap_connection",
522
+ message_id: message_id, messages: messages
523
+ end
524
+ end
525
+
526
+ MODIFY_OPERATIONS = { #:nodoc:
527
+ :add => 0,
528
+ :delete => 1,
529
+ :replace => 2,
530
+ }
531
+
532
+ def self.modify_ops(operations)
533
+ ops = []
534
+ if operations
535
+ operations.each do |op, attrib, values|
536
+ # TODO, fix the following line, which gives a bogus error if the
537
+ # opcode is invalid.
538
+ op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
539
+ values = [values].flatten.map { |v| v.to_ber if v }.to_ber_set
540
+ values = [attrib.to_s.to_ber, values].to_ber_sequence
541
+ ops << [op_ber, values].to_ber
542
+ end
543
+ end
544
+ ops
545
+ end
546
+
547
+ #--
548
+ # TODO: need to support a time limit, in case the server fails to respond.
549
+ # TODO: We're throwing an exception here on empty DN. Should return a
550
+ # proper error instead, probaby from farther up the chain.
551
+ # TODO: If the user specifies a bogus opcode, we'll throw a confusing
552
+ # error here ("to_ber_enumerated is not defined on nil").
553
+ #++
554
+ def modify(args)
555
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
556
+ ops = self.class.modify_ops args[:operations]
557
+
558
+ message_id = next_msgid
559
+ request = [
560
+ modify_dn.to_ber,
561
+ ops.to_ber_sequence,
562
+ ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
563
+
564
+ write(request, nil, message_id)
565
+ pdu = queued_read(message_id)
566
+
567
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
568
+ raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
569
+ end
570
+
571
+ pdu
572
+ end
573
+
574
+ ##
575
+ # Password Modify
576
+ #
577
+ # http://tools.ietf.org/html/rfc3062
578
+ #
579
+ # passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1
580
+ #
581
+ # PasswdModifyRequestValue ::= SEQUENCE {
582
+ # userIdentity [0] OCTET STRING OPTIONAL
583
+ # oldPasswd [1] OCTET STRING OPTIONAL
584
+ # newPasswd [2] OCTET STRING OPTIONAL }
585
+ #
586
+ # PasswdModifyResponseValue ::= SEQUENCE {
587
+ # genPasswd [0] OCTET STRING OPTIONAL }
588
+ #
589
+ # Encoded request:
590
+ #
591
+ # 00\x02\x01\x02w+\x80\x171.3.6.1.4.1.4203.1.11.1\x81\x100\x0E\x81\x05old\x82\x05new
592
+ #
593
+ def password_modify(args)
594
+ dn = args[:dn]
595
+ raise ArgumentError, 'DN is required' if !dn || dn.empty?
596
+
597
+ ext_seq = [Net::LDAP::PasswdModifyOid.to_ber_contextspecific(0)]
598
+
599
+ pwd_seq = []
600
+ pwd_seq << dn.to_ber(0x80)
601
+ pwd_seq << args[:old_password].to_ber(0x81) unless args[:old_password].nil?
602
+ pwd_seq << args[:new_password].to_ber(0x82) unless args[:new_password].nil?
603
+ ext_seq << pwd_seq.to_ber_sequence.to_ber(0x81)
604
+
605
+ request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
606
+
607
+ message_id = next_msgid
608
+
609
+ write(request, nil, message_id)
610
+ pdu = queued_read(message_id)
611
+
612
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
613
+ raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
614
+ end
615
+
616
+ pdu
617
+ end
618
+
619
+ #--
620
+ # TODO: need to support a time limit, in case the server fails to respond.
621
+ # Unlike other operation-methods in this class, we return a result hash
622
+ # rather than a simple result number. This is experimental, and eventually
623
+ # we'll want to do this with all the others. The point is to have access
624
+ # to the error message and the matched-DN returned by the server.
625
+ #++
626
+ def add(args)
627
+ add_dn = args[:dn] or raise Net::LDAP::EmptyDNError, "Unable to add empty DN"
628
+ add_attrs = []
629
+ a = args[:attributes] and a.each do |k, v|
630
+ add_attrs << [k.to_s.to_ber, Array(v).map(&:to_ber).to_ber_set].to_ber_sequence
631
+ end
632
+
633
+ message_id = next_msgid
634
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
635
+
636
+ write(request, nil, message_id)
637
+ pdu = queued_read(message_id)
638
+
639
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
640
+ raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
641
+ end
642
+
643
+ pdu
644
+ end
645
+
646
+ #--
647
+ # TODO: need to support a time limit, in case the server fails to respond.
648
+ #++
649
+ def rename(args)
650
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
651
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
652
+ delete_attrs = args[:delete_attributes] ? true : false
653
+ new_superior = args[:new_superior]
654
+
655
+ message_id = next_msgid
656
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
657
+ request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
658
+
659
+ write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id)
660
+ pdu = queued_read(message_id)
661
+
662
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse
663
+ raise Net::LDAP::ResponseMissingOrInvalidError.new "response missing or invalid"
664
+ end
665
+
666
+ pdu
667
+ end
668
+
669
+ #--
670
+ # TODO, need to support a time limit, in case the server fails to respond.
671
+ #++
672
+ def delete(args)
673
+ dn = args[:dn] or raise "Unable to delete empty DN"
674
+ controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
675
+ message_id = next_msgid
676
+ request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest)
677
+
678
+ write(request, controls, message_id)
679
+ pdu = queued_read(message_id)
680
+
681
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse
682
+ raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
683
+ end
684
+
685
+ pdu
686
+ end
687
+
688
+ # Internal: Returns a Socket like object used internally to communicate with
689
+ # LDAP server.
690
+ #
691
+ # Typically a TCPSocket, but can be a OpenSSL::SSL::SSLSocket
692
+ def socket
693
+ return @conn if defined?(@conn) && !@conn.nil?
694
+
695
+ # First refactoring uses the existing methods open_connection and
696
+ # prepare_socket to set @conn. Next cleanup would centralize connection
697
+ # handling here.
698
+ if @server[:socket]
699
+ prepare_socket(@server)
700
+ else
701
+ @server[:hosts] = [[@server[:host], @server[:port]]] if @server[:hosts].nil?
702
+ open_connection(@server)
703
+ end
704
+
705
+ @conn
706
+ end
707
+
708
+ private
709
+
710
+ # Wrap around Socket.tcp to normalize with other Socket initializers
711
+ class DefaultSocket
712
+ def self.new(host, port, socket_opts = {})
713
+ Socket.tcp(host, port, **socket_opts)
714
+ end
715
+ end
716
+ end # class Connection