ruby-ldapserver 0.3.1

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.
@@ -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,273 @@
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
+ tag = blk[0] & 0x1f
53
+ len = blk[1]
54
+
55
+ if tag == 0x1f # long form
56
+ tag = 0
57
+ while true
58
+ ch = io.getc
59
+ blk << ch
60
+ tag = (tag << 7) | (ch & 0x7f)
61
+ break if (ch & 0x80) == 0
62
+ end
63
+ len = io.getc
64
+ blk << len
65
+ end
66
+
67
+ if (len & 0x80) != 0 # long form
68
+ len = len & 0x7f
69
+ raise LDAP::ResultError::ProtocolError, "Indefinite length encoding not supported" if len == 0
70
+ offset = blk.length
71
+ blk << io.read(len)
72
+ # is there a more efficient way of doing this?
73
+ len = 0
74
+ blk[offset..-1].each_byte { |b| len = (len << 8) | b }
75
+ end
76
+
77
+ offset = blk.length
78
+ blk << io.read(len)
79
+ return blk
80
+ # or if we wanted to keep the partial decoding we've done:
81
+ # return blk, [blk[0] >> 6, tag], offset
82
+ end
83
+
84
+ def handle_requests
85
+ operationClass = @opt[:operation_class]
86
+ ocArgs = @opt[:operation_args] || []
87
+ catch(:close) do
88
+ while true
89
+ begin
90
+ blk = ber_read(@io)
91
+ asn1 = OpenSSL::ASN1::decode(blk)
92
+ # Debugging:
93
+ # puts "Request: #{blk.unpack("H*")}\n#{asn1.inspect}" if $debug
94
+
95
+ raise LDAP::ResultError::ProtocolError, "LDAPMessage must be SEQUENCE" unless asn1.is_a?(OpenSSL::ASN1::Sequence)
96
+ raise LDAP::ResultError::ProtocolError, "Bad Message ID" unless asn1.value[0].is_a?(OpenSSL::ASN1::Integer)
97
+ messageId = asn1.value[0].value
98
+
99
+ protocolOp = asn1.value[1]
100
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp" unless protocolOp.is_a?(OpenSSL::ASN1::ASN1Data)
101
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp tag class" unless protocolOp.tag_class == :APPLICATION
102
+
103
+ # controls are not properly implemented
104
+ c = asn1.value[2]
105
+ if c.is_a?(OpenSSL::ASN1::ASN1Data) and c.tag_class == :APPLICATION and c.tag == 0
106
+ controls = c.value
107
+ end
108
+
109
+ case protocolOp.tag
110
+ when 0 # BindRequest
111
+ abandon_all
112
+ @binddn, @version = operationClass.new(self,messageId,*ocArgs).
113
+ do_bind(protocolOp, controls)
114
+
115
+ when 2 # UnbindRequest
116
+ throw(:close)
117
+
118
+ when 3 # SearchRequest
119
+ # Note: RFC 2251 4.4.4.1 says behaviour is undefined if
120
+ # client sends an overlapping request with same message ID,
121
+ # so we don't have to worry about the case where there is
122
+ # already a thread with this id in @active_reqs.
123
+ # However, to avoid a race we copy messageId/
124
+ # protocolOp/controls into thread-local variables, because
125
+ # they will change when the next request comes in.
126
+ #
127
+ # There is a theoretical race condition here: a client could
128
+ # send an abandon request before Thread.current is assigned to
129
+ # @active_reqs[thrm]. It's not a problem, because abandon isn't
130
+ # guaranteed to work anyway. Doing it this way ensures that
131
+ # @active_reqs does not leak memory on a long-lived connection.
132
+
133
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
134
+ begin
135
+ @active_reqs[thrm] = Thread.current
136
+ operationClass.new(self,thrm,*ocArgs).do_search(thrp, thrc)
137
+ ensure
138
+ @active_reqs.delete(thrm)
139
+ end
140
+ end
141
+
142
+ when 6 # ModifyRequest
143
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
144
+ begin
145
+ @active_reqs[thrm] = Thread.current
146
+ operationClass.new(self,thrm,*ocArgs).do_modify(thrp, thrc)
147
+ ensure
148
+ @active_reqs.delete(thrm)
149
+ end
150
+ end
151
+
152
+ when 8 # AddRequest
153
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
154
+ begin
155
+ @active_reqs[thrm] = Thread.current
156
+ operationClass.new(self,thrm,*ocArgs).do_add(thrp, thrc)
157
+ ensure
158
+ @active_reqs.delete(thrm)
159
+ end
160
+ end
161
+
162
+ when 10 # DelRequest
163
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
164
+ begin
165
+ @active_reqs[thrm] = Thread.current
166
+ operationClass.new(self,thrm,*ocArgs).do_del(thrp, thrc)
167
+ ensure
168
+ @active_reqs.delete(thrm)
169
+ end
170
+ end
171
+
172
+ when 12 # ModifyDNRequest
173
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
174
+ begin
175
+ @active_reqs[thrm] = Thread.current
176
+ operationClass.new(self,thrm,*ocArgs).do_modifydn(thrp, thrc)
177
+ ensure
178
+ @active_reqs.delete(thrm)
179
+ end
180
+ end
181
+
182
+ when 14 # CompareRequest
183
+ Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
184
+ begin
185
+ @active_reqs[thrm] = Thread.current
186
+ operationClass.new(self,thrm,*ocArgs).do_compare(thrp, thrc)
187
+ ensure
188
+ @active_reqs.delete(thrm)
189
+ end
190
+ end
191
+
192
+ when 16 # AbandonRequest
193
+ abandon(protocolOp.value)
194
+
195
+ else
196
+ raise LDAP::ResultError::ProtocolError, "Unrecognised protocolOp tag #{protocolOp.tag}"
197
+ end
198
+
199
+ rescue LDAP::ResultError::ProtocolError, OpenSSL::ASN1::ASN1Error => e
200
+ send_notice_of_disconnection(LDAP::ResultError::ProtocolError.new.to_i, e.message)
201
+ throw(:close)
202
+
203
+ # all other exceptions propagate up and are caught by tcpserver
204
+ end
205
+ end
206
+ end
207
+ abandon_all
208
+ end
209
+
210
+ def write(data)
211
+ @mutex.synchronize do
212
+ @io.write(data)
213
+ @io.flush
214
+ end
215
+ end
216
+
217
+ def writelock
218
+ @mutex.synchronize do
219
+ yield @io
220
+ @io.flush
221
+ end
222
+ end
223
+
224
+ def abandon(messageID)
225
+ @mutex.synchronize do
226
+ thread = @active_reqs.delete(messageID)
227
+ thread.raise LDAP::Abandon if thread and thread.alive?
228
+ end
229
+ end
230
+
231
+ def abandon_all
232
+ return if @active_reqs.size == 0
233
+ @mutex.synchronize do
234
+ @active_reqs.each do |id, thread|
235
+ thread.raise LDAP::Abandon if thread.alive?
236
+ end
237
+ @active_reqs = {}
238
+ end
239
+ end
240
+
241
+ def send_unsolicited_notification(resultCode, opt={})
242
+ protocolOp = [
243
+ OpenSSL::ASN1::Enumerated(resultCode),
244
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
245
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
246
+ ]
247
+ if opt[:referral]
248
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
249
+ protocolOp << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
250
+ end
251
+ if opt[:responseName]
252
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
253
+ end
254
+ if opt[:response]
255
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
256
+ end
257
+ message = [
258
+ OpenSSL::ASN1::Integer(0),
259
+ OpenSSL::ASN1::Sequence(protocolOp, 24, :IMPLICIT, :APPLICATION),
260
+ ]
261
+ message << opt[:controls] if opt[:controls]
262
+ write(OpenSSL::ASN1::Sequence(message).to_der)
263
+ end
264
+
265
+ def send_notice_of_disconnection(resultCode, errorMessage="")
266
+ send_unsolicited_notification(resultCode,
267
+ :errorMessage=>errorMessage,
268
+ :responseName=>"1.3.6.1.4.1.1466.20036"
269
+ )
270
+ end
271
+ end
272
+ end # class Server
273
+ 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