fakeldap 0.0.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 (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +3 -0
  3. data/lib/fakeldap.rb +46 -0
  4. data/lib/fakeldap/version.rb +4 -0
  5. data/vendor/ruby-ldapserver/COPYING +27 -0
  6. data/vendor/ruby-ldapserver/ChangeLog +83 -0
  7. data/vendor/ruby-ldapserver/Manifest.txt +32 -0
  8. data/vendor/ruby-ldapserver/README +222 -0
  9. data/vendor/ruby-ldapserver/Rakefile +22 -0
  10. data/vendor/ruby-ldapserver/examples/README +89 -0
  11. data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
  12. data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
  13. data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
  14. data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
  15. data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
  16. data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
  17. data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
  18. data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
  19. data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
  20. data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
  21. data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
  22. data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
  23. data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
  24. data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
  25. data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
  26. data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
  27. data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
  28. data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
  29. data/vendor/ruby-ldapserver/test/core.schema +582 -0
  30. data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
  31. data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
  32. data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
  33. data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
  34. data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
  35. data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
  36. data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
  37. metadata +130 -0
@@ -0,0 +1,37 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'ldap'
4
+
5
+ CHILDREN = 10
6
+ CONNECTS = 1 # per child
7
+ SEARCHES = 100 # per connection
8
+
9
+ pids = []
10
+ CHILDREN.times do
11
+ pids << fork do
12
+ CONNECTS.times do
13
+ conn = LDAP::Conn.new("localhost",1389)
14
+ conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
15
+ conn.bind
16
+ SEARCHES.times do
17
+ res = conn.search("cn=Fred Flintstone,dc=example,dc=com", LDAP::LDAP_SCOPE_BASE,
18
+ "(objectclass=*)") do |e|
19
+ #puts "#{$$} #{e.dn.inspect}"
20
+ end
21
+ end
22
+ conn.unbind
23
+ end
24
+ end
25
+ end
26
+ okcount = 0
27
+ badcount = 0
28
+ pids.each do |p|
29
+ Process.wait(p)
30
+ if $?.exitstatus == 0
31
+ okcount += 1
32
+ else
33
+ badcount += 1
34
+ end
35
+ end
36
+ puts "Children finished: #{okcount} ok, #{badcount} failed"
37
+ exit badcount > 0 ? 1 : 0
@@ -0,0 +1,4 @@
1
+ require 'ldap/server/result'
2
+ require 'ldap/server/connection'
3
+ require 'ldap/server/operation'
4
+ require 'ldap/server/server'
@@ -0,0 +1,276 @@
1
+ require 'thread'
2
+ require 'openssl'
3
+ require 'ldap/server/result'
4
+
5
+ module LDAP
6
+ class Server
7
+
8
+ # An object which handles an LDAP connection. Note that LDAP allows
9
+ # requests and responses to be exchanged asynchronously: e.g. a client
10
+ # can send three requests, and the three responses can come back in
11
+ # any order. For that reason, we start a new thread for each request,
12
+ # and we need a mutex on the io object so that multiple responses don't
13
+ # interfere with each other.
14
+
15
+ class Connection
16
+ attr_reader :binddn, :version, :opt
17
+
18
+ def initialize(io, opt={})
19
+ @io = io
20
+ @opt = opt
21
+ @mutex = Mutex.new
22
+ @active_reqs = {} # map message ID to thread object
23
+ @binddn = nil
24
+ @version = 3
25
+ @logger = @opt[:logger] || $stderr
26
+ @ssl = false
27
+
28
+ startssl if @opt[:ssl_on_connect]
29
+ end
30
+
31
+ def log(msg)
32
+ @logger << "[#{@io.peeraddr[3]}]: #{msg}\n"
33
+ end
34
+
35
+ def startssl # :yields:
36
+ @mutex.synchronize do
37
+ raise LDAP::ResultError::OperationsError if @ssl or @active_reqs.size > 0
38
+ yield if block_given?
39
+ @io = OpenSSL::SSL::SSLSocket.new(@io, @opt[:ssl_ctx])
40
+ @io.sync_close = true
41
+ @io.accept
42
+ @ssl = true
43
+ end
44
+ end
45
+
46
+ # Read one ASN1 element from the given stream.
47
+ # Return String containing the raw element.
48
+
49
+ def ber_read(io)
50
+ blk = io.read(2) # minimum: short tag, short length
51
+ throw(:close) if blk.nil?
52
+
53
+ codepoints = blk.respond_to?(:codepoints) ? blk.codepoints.to_a : blk
54
+
55
+ tag = codepoints[0] & 0x1f
56
+ len = codepoints[1]
57
+
58
+ if tag == 0x1f # long form
59
+ tag = 0
60
+ while true
61
+ ch = io.getc
62
+ blk << ch
63
+ tag = (tag << 7) | (ch & 0x7f)
64
+ break if (ch & 0x80) == 0
65
+ end
66
+ len = io.getc
67
+ blk << len
68
+ end
69
+
70
+ if (len & 0x80) != 0 # long form
71
+ len = len & 0x7f
72
+ raise LDAP::ResultError::ProtocolError, "Indefinite length encoding not supported" if len == 0
73
+ offset = blk.length
74
+ blk << io.read(len)
75
+ # is there a more efficient way of doing this?
76
+ len = 0
77
+ blk[offset..-1].each_byte { |b| len = (len << 8) | b }
78
+ end
79
+
80
+ offset = blk.length
81
+ blk << io.read(len)
82
+ return blk
83
+ # or if we wanted to keep the partial decoding we've done:
84
+ # return blk, [blk[0] >> 6, tag], offset
85
+ end
86
+
87
+ def handle_requests
88
+ operationClass = @opt[:operation_class]
89
+ ocArgs = @opt[:operation_args] || []
90
+ catch(:close) do
91
+ while true
92
+ begin
93
+ blk = ber_read(@io)
94
+ asn1 = OpenSSL::ASN1::decode(blk)
95
+ # Debugging:
96
+ # puts "Request: #{blk.unpack("H*")}\n#{asn1.inspect}" if $debug
97
+
98
+ raise LDAP::ResultError::ProtocolError, "LDAPMessage must be SEQUENCE" unless asn1.is_a?(OpenSSL::ASN1::Sequence)
99
+ raise LDAP::ResultError::ProtocolError, "Bad Message ID" unless asn1.value[0].is_a?(OpenSSL::ASN1::Integer)
100
+ messageId = asn1.value[0].value
101
+
102
+ protocolOp = asn1.value[1]
103
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp" unless protocolOp.is_a?(OpenSSL::ASN1::ASN1Data)
104
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp tag class" unless protocolOp.tag_class == :APPLICATION
105
+
106
+ # controls are not properly implemented
107
+ c = asn1.value[2]
108
+ if c.is_a?(OpenSSL::ASN1::ASN1Data) and c.tag_class == :APPLICATION and c.tag == 0
109
+ controls = c.value
110
+ end
111
+
112
+ case protocolOp.tag
113
+ when 0 # BindRequest
114
+ abandon_all
115
+ @binddn, @version = operationClass.new(self,messageId,*ocArgs).
116
+ do_bind(protocolOp, controls)
117
+
118
+ when 2 # UnbindRequest
119
+ throw(:close)
120
+
121
+ when 3 # SearchRequest
122
+ # Note: RFC 2251 4.4.4.1 says behaviour is undefined if
123
+ # client sends an overlapping request with same message ID,
124
+ # so we don't have to worry about the case where there is
125
+ # already a thread with this id in @active_reqs.
126
+ # However, to avoid a race we copy messageId/
127
+ # protocolOp/controls into thread-local variables, because
128
+ # they will change when the next request comes in.
129
+ #
130
+ # There is a theoretical race condition here: a client could
131
+ # send an abandon request before Thread.current is assigned to
132
+ # @active_reqs[thrm]. It's not a problem, because abandon isn't
133
+ # guaranteed to work anyway. Doing it this way ensures that
134
+ # @active_reqs does not leak memory on a long-lived connection.
135
+
136
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
137
+ begin
138
+ @active_reqs[thrm] = Thread.current
139
+ operationClass.new(self,thrm,*ocArgs).do_search(thrp, thrc)
140
+ ensure
141
+ @active_reqs.delete(thrm)
142
+ end
143
+ end
144
+
145
+ when 6 # ModifyRequest
146
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
147
+ begin
148
+ @active_reqs[thrm] = Thread.current
149
+ operationClass.new(self,thrm,*ocArgs).do_modify(thrp, thrc)
150
+ ensure
151
+ @active_reqs.delete(thrm)
152
+ end
153
+ end
154
+
155
+ when 8 # AddRequest
156
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
157
+ begin
158
+ @active_reqs[thrm] = Thread.current
159
+ operationClass.new(self,thrm,*ocArgs).do_add(thrp, thrc)
160
+ ensure
161
+ @active_reqs.delete(thrm)
162
+ end
163
+ end
164
+
165
+ when 10 # DelRequest
166
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
167
+ begin
168
+ @active_reqs[thrm] = Thread.current
169
+ operationClass.new(self,thrm,*ocArgs).do_del(thrp, thrc)
170
+ ensure
171
+ @active_reqs.delete(thrm)
172
+ end
173
+ end
174
+
175
+ when 12 # ModifyDNRequest
176
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
177
+ begin
178
+ @active_reqs[thrm] = Thread.current
179
+ operationClass.new(self,thrm,*ocArgs).do_modifydn(thrp, thrc)
180
+ ensure
181
+ @active_reqs.delete(thrm)
182
+ end
183
+ end
184
+
185
+ when 14 # CompareRequest
186
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
187
+ begin
188
+ @active_reqs[thrm] = Thread.current
189
+ operationClass.new(self,thrm,*ocArgs).do_compare(thrp, thrc)
190
+ ensure
191
+ @active_reqs.delete(thrm)
192
+ end
193
+ end
194
+
195
+ when 16 # AbandonRequest
196
+ abandon(protocolOp.value)
197
+
198
+ else
199
+ raise LDAP::ResultError::ProtocolError, "Unrecognised protocolOp tag #{protocolOp.tag}"
200
+ end
201
+
202
+ rescue LDAP::ResultError::ProtocolError, OpenSSL::ASN1::ASN1Error => e
203
+ send_notice_of_disconnection(LDAP::ResultError::ProtocolError.new.to_i, e.message)
204
+ throw(:close)
205
+
206
+ # all other exceptions propagate up and are caught by tcpserver
207
+ end
208
+ end
209
+ end
210
+ abandon_all
211
+ end
212
+
213
+ def write(data)
214
+ @mutex.synchronize do
215
+ @io.write(data)
216
+ @io.flush
217
+ end
218
+ end
219
+
220
+ def writelock
221
+ @mutex.synchronize do
222
+ yield @io
223
+ @io.flush
224
+ end
225
+ end
226
+
227
+ def abandon(messageID)
228
+ @mutex.synchronize do
229
+ thread = @active_reqs.delete(messageID)
230
+ thread.raise LDAP::Abandon if thread and thread.alive?
231
+ end
232
+ end
233
+
234
+ def abandon_all
235
+ return if @active_reqs.size == 0
236
+ @mutex.synchronize do
237
+ @active_reqs.each do |id, thread|
238
+ thread.raise LDAP::Abandon if thread.alive?
239
+ end
240
+ @active_reqs = {}
241
+ end
242
+ end
243
+
244
+ def send_unsolicited_notification(resultCode, opt={})
245
+ protocolOp = [
246
+ OpenSSL::ASN1::Enumerated(resultCode),
247
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
248
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
249
+ ]
250
+ if opt[:referral]
251
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
252
+ protocolOp << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
253
+ end
254
+ if opt[:responseName]
255
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
256
+ end
257
+ if opt[:response]
258
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
259
+ end
260
+ message = [
261
+ OpenSSL::ASN1::Integer(0),
262
+ OpenSSL::ASN1::Sequence(protocolOp, 24, :IMPLICIT, :APPLICATION),
263
+ ]
264
+ message << opt[:controls] if opt[:controls]
265
+ write(OpenSSL::ASN1::Sequence(message).to_der)
266
+ end
267
+
268
+ def send_notice_of_disconnection(resultCode, errorMessage="")
269
+ send_unsolicited_notification(resultCode,
270
+ :errorMessage=>errorMessage,
271
+ :responseName=>"1.3.6.1.4.1.1466.20036"
272
+ )
273
+ end
274
+ end
275
+ end # class Server
276
+ end # module LDAP
@@ -0,0 +1,223 @@
1
+ require 'ldap/server/result'
2
+ require 'ldap/server/match'
3
+
4
+ module LDAP
5
+ class Server
6
+
7
+ # LDAP filters are parsed into a LISP-like internal representation:
8
+ #
9
+ # [:true]
10
+ # [:false]
11
+ # [:undef]
12
+ # [:and, ..., ..., ...]
13
+ # [:or, ..., ..., ...]
14
+ # [:not, ...]
15
+ # [:present, attr]
16
+ # [:eq, attr, MO, val]
17
+ # [:approx, attr, MO, val]
18
+ # [:substrings, attr, MO, initial=nil, {any, any...}, final=nil]
19
+ # [:ge, attr, MO, val]
20
+ # [:le, attr, MO, val]
21
+ #
22
+ # This is done rather than a more object-oriented approach, in the
23
+ # hope that it will make it easier to match certain filter structures
24
+ # when converting them into something else. e.g. certain LDAP filter
25
+ # constructs can be mapped to some fixed SQL queries.
26
+ #
27
+ # See RFC 2251 4.5.1 for the three-state(!) boolean logic from LDAP
28
+ #
29
+ # If no schema is provided: 'attr' is the raw attribute name as provided
30
+ # by the client. If a schema is provided: attr is converted to its
31
+ # normalized name as listed in the schema, e.g. 'commonname' becomes 'cn',
32
+ # 'objectclass' becomes 'objectClass' etc.
33
+ # If a schema is provided, MO is a matching object which can be used to
34
+ # perform the match. If no schema is provided, this is 'nil'. In that
35
+ # case you could use LDAP::Server::MatchingRule::DefaultMatch.
36
+
37
+ class Filter
38
+
39
+ # Parse a filter in OpenSSL::ASN1 format into our own format.
40
+ #
41
+ # There are some trivial optimisations we make: e.g.
42
+ # (&(objectClass=*)(cn=foo)) -> (&(cn=foo)) -> (cn=foo)
43
+
44
+ def self.parse(asn1, schema=nil)
45
+ case asn1.tag
46
+ when 0 # and
47
+ conds = asn1.value.collect { |a| parse(a) }
48
+ conds.delete([:true])
49
+ return [:true] if conds.size == 0
50
+ return conds.first if conds.size == 1
51
+ return [:false] if conds.include?([:false])
52
+ return conds.unshift(:and)
53
+
54
+ when 1 # or
55
+ conds = asn1.value.collect { |a| parse(a) }
56
+ conds.delete([:false])
57
+ return [:false] if conds.size == 0
58
+ return conds.first if conds.size == 1
59
+ return [:true] if conds.include?([:true])
60
+ return conds.unshift(:or)
61
+
62
+ when 2 # not
63
+ cond = parse(asn1.value[0])
64
+ case cond
65
+ when [:false]; return [:true]
66
+ when [:true]; return [:false]
67
+ when [:undef]; return [:undef]
68
+ end
69
+ return [:not, cond]
70
+
71
+ when 3 # equalityMatch
72
+ attr = asn1.value[0].value
73
+ val = asn1.value[1].value
74
+ return [:true] if attr =~ /\AobjectClass\z/i and val =~ /\Atop\z/i
75
+ if schema
76
+ a = schema.find_attrtype(attr)
77
+ return [:undef] unless a.equality
78
+ return [:eq, a.to_s, a.equality, val]
79
+ end
80
+ return [:eq, attr, nil, val]
81
+
82
+ when 4 # substrings
83
+ attr = asn1.value[0].value
84
+ if schema
85
+ a = schema.find_attrtype(attr)
86
+ return [:undef] unless a.substr
87
+ res = [:substrings, a.to_s, a.substr, nil]
88
+ else
89
+ res = [:substrings, attr, nil, nil]
90
+ end
91
+ final_val = nil
92
+
93
+ asn1.value[1].value.each do |ss|
94
+ case ss.tag
95
+ when 0
96
+ res[3] = ss.value
97
+ when 1
98
+ res << ss.value
99
+ when 2
100
+ final_val = ss.value
101
+ else
102
+ raise LDAP::ResultError::ProtocolError,
103
+ "Unrecognised substring tag #{ss.tag.inspect}"
104
+ end
105
+ end
106
+ res << final_val
107
+ return res
108
+
109
+ when 5 # greaterOrEqual
110
+ attr = asn1.value[0].value
111
+ val = asn1.value[1].value
112
+ if schema
113
+ a = schema.find_attrtype(attr)
114
+ return [:undef] unless a.ordering
115
+ return [:ge, a.to_s, a.ordering, val]
116
+ end
117
+ return [:ge, attr, nil, val]
118
+
119
+ when 6 # lessOrEqual
120
+ attr = asn1.value[0].value
121
+ val = asn1.value[1].value
122
+ if schema
123
+ a = schema.find_attrtype(attr)
124
+ return [:undef] unless a.ordering
125
+ return [:le, a.to_s, a.ordering, val]
126
+ end
127
+ return [:le, attr, nil, val]
128
+
129
+ when 7 # present
130
+ attr = asn1.value
131
+ return [:true] if attr =~ /\AobjectClass\z/i
132
+ if schema
133
+ begin
134
+ a = schema.find_attrtype(attr)
135
+ return [:present, a.to_s]
136
+ rescue LDAP::ResultError::UndefinedAttributeType
137
+ return [:false]
138
+ end
139
+ end
140
+ return [:present, attr]
141
+
142
+ when 8 # approxMatch
143
+ attr = asn1.value[0].value
144
+ val = asn1.value[1].value
145
+ if schema
146
+ a = schema.find_attrtype(attr)
147
+ # I don't know how properly to deal with approxMatch. I'm assuming
148
+ # that the object will have an equality MatchingRule, and we
149
+ # can defer to that.
150
+ return [:undef] unless a.equality
151
+ return [:approx, a.to_s, a.equality, val]
152
+ end
153
+ return [:approx, attr, nil, val]
154
+
155
+ #when 9 # extensibleMatch
156
+ # FIXME
157
+
158
+ else
159
+ raise LDAP::ResultError::ProtocolError,
160
+ "Unrecognised Filter tag #{asn1.tag}"
161
+ end
162
+
163
+ # Unknown attribute type
164
+ rescue LDAP::ResultError::UndefinedAttributeType
165
+ return [:undef]
166
+ end
167
+
168
+ # Run a parsed filter against an attr=>[val] hash.
169
+ #
170
+ # Returns true, false or nil.
171
+
172
+ def self.run(filter, av)
173
+ case filter[0]
174
+ when :and
175
+ res = true
176
+ filter[1..-1].each do |elem|
177
+ r = run(elem, av)
178
+ return false if r == false
179
+ res = nil if r.nil?
180
+ end
181
+ return res
182
+
183
+ when :or
184
+ res = false
185
+ filter[1..-1].each do |elem|
186
+ r = run(elem, av)
187
+ return true if r == true
188
+ res = nil if r.nil?
189
+ end
190
+ return res
191
+
192
+ when :not
193
+ case run(filter[1], av)
194
+ when true; return false
195
+ when false; return true
196
+ else return nil
197
+ end
198
+
199
+ when :present
200
+ return av.has_key?(filter[1])
201
+
202
+ when :eq, :approx, :le, :ge, :substrings
203
+ # the filter now includes a suitable matching object
204
+ return (filter[2] || LDAP::Server::MatchingRule::DefaultMatch).send(
205
+ filter.first, av[filter[1].to_s], *filter[3..-1])
206
+
207
+ when :true
208
+ return true
209
+
210
+ when :false
211
+ return false
212
+
213
+ when :undef
214
+ return nil
215
+ end
216
+
217
+ raise LDAP::ResultError::OperationsError,
218
+ "Unimplemented filter #{filter.first.inspect}"
219
+ end
220
+
221
+ end # class Filter
222
+ end # class Server
223
+ end # module LDAP