ruby-ldapserver 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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