deveo-ruby-ldapserver 0.5.2

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,251 @@
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
+ @threadgroup = ThreadGroup.new
23
+ @binddn = nil
24
+ @version = 3
25
+ @logger = @opt[:logger]
26
+ @ssl = false
27
+
28
+ startssl if @opt[:ssl_on_connect]
29
+ end
30
+
31
+ def log(msg, severity = Logger::INFO)
32
+ @logger.add(severity, msg, @io.peeraddr[3])
33
+ end
34
+
35
+ def debug msg
36
+ log msg, Logger::DEBUG
37
+ end
38
+
39
+ def log_exception(e)
40
+ log "#{e}: #{e.backtrace.join("\n\tfrom ")}", Logger::ERROR
41
+ end
42
+
43
+ def startssl # :yields:
44
+ @mutex.synchronize do
45
+ raise LDAP::ResultError::OperationsError if @ssl or @threadgroup.list.size > 0
46
+ yield if block_given?
47
+ @io = OpenSSL::SSL::SSLSocket.new(@io, @opt[:ssl_ctx])
48
+ @io.sync_close = true
49
+ @io.accept
50
+ @ssl = true
51
+ end
52
+ end
53
+
54
+ # Read one ASN1 element from the given stream.
55
+ # Return String containing the raw element.
56
+
57
+ def ber_read(io)
58
+ blk = io.read(2) # minimum: short tag, short length
59
+ throw(:close) if blk.nil?
60
+
61
+ codepoints = blk.respond_to?(:codepoints) ? blk.codepoints.to_a : blk
62
+
63
+ tag = codepoints[0] & 0x1f
64
+ len = codepoints[1]
65
+
66
+ if tag == 0x1f # long form
67
+ tag = 0
68
+ while true
69
+ ch = io.getc
70
+ blk << ch
71
+ tag = (tag << 7) | (ch & 0x7f)
72
+ break if (ch & 0x80) == 0
73
+ end
74
+ len = io.getc
75
+ blk << len
76
+ end
77
+
78
+ if (len & 0x80) != 0 # long form
79
+ len = len & 0x7f
80
+ raise LDAP::ResultError::ProtocolError, "Indefinite length encoding not supported" if len == 0
81
+ offset = blk.length
82
+ blk << io.read(len)
83
+ # is there a more efficient way of doing this?
84
+ len = 0
85
+ blk[offset..-1].each_byte { |b| len = (len << 8) | b }
86
+ end
87
+
88
+ offset = blk.length
89
+ blk << io.read(len)
90
+ return blk
91
+ # or if we wanted to keep the partial decoding we've done:
92
+ # return blk, [blk[0] >> 6, tag], offset
93
+ end
94
+
95
+ def handle_requests
96
+ operationClass = @opt[:operation_class]
97
+ ocArgs = @opt[:operation_args] || []
98
+ catch(:close) do
99
+ while true
100
+ begin
101
+ blk = ber_read(@io)
102
+ asn1 = OpenSSL::ASN1::decode(blk)
103
+ # Debugging:
104
+ # puts "Request: #{blk.unpack("H*")}\n#{asn1.inspect}" if $debug
105
+
106
+ raise LDAP::ResultError::ProtocolError, "LDAPMessage must be SEQUENCE" unless asn1.is_a?(OpenSSL::ASN1::Sequence)
107
+ raise LDAP::ResultError::ProtocolError, "Bad Message ID" unless asn1.value[0].is_a?(OpenSSL::ASN1::Integer)
108
+ messageId = asn1.value[0].value
109
+
110
+ protocolOp = asn1.value[1]
111
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp" unless protocolOp.is_a?(OpenSSL::ASN1::ASN1Data)
112
+ raise LDAP::ResultError::ProtocolError, "Bad protocolOp tag class" unless protocolOp.tag_class == :APPLICATION
113
+
114
+ # controls are not properly implemented
115
+ c = asn1.value[2]
116
+ if c.is_a?(OpenSSL::ASN1::ASN1Data) and c.tag_class == :APPLICATION and c.tag == 0
117
+ controls = c.value
118
+ end
119
+
120
+ case protocolOp.tag
121
+ when 0 # BindRequest
122
+ abandon_all
123
+ @binddn, @version = operationClass.new(self,messageId,*ocArgs).
124
+ do_bind(protocolOp, controls)
125
+
126
+ when 2 # UnbindRequest
127
+ throw(:close)
128
+
129
+ when 3 # SearchRequest
130
+ start_op(messageId,protocolOp,controls,:do_search)
131
+
132
+ when 6 # ModifyRequest
133
+ start_op(messageId,protocolOp,controls,:do_modify)
134
+
135
+ when 8 # AddRequest
136
+ start_op(messageId,protocolOp,controls,:do_add)
137
+
138
+ when 10 # DelRequest
139
+ start_op(messageId,protocolOp,controls,:do_del)
140
+
141
+ when 12 # ModifyDNRequest
142
+ start_op(messageId,protocolOp,controls,:do_modifydn)
143
+
144
+ when 14 # CompareRequest
145
+ start_op(messageId,protocolOp,controls,:do_compare)
146
+
147
+ when 16 # AbandonRequest
148
+ abandon(messageId)
149
+
150
+ else
151
+ raise LDAP::ResultError::ProtocolError, "Unrecognised protocolOp tag #{protocolOp.tag}"
152
+ end
153
+
154
+ rescue LDAP::ResultError::ProtocolError, OpenSSL::ASN1::ASN1Error => e
155
+ send_notice_of_disconnection(LDAP::ResultError::ProtocolError.new.to_i, e.message)
156
+ throw(:close)
157
+
158
+ # all other exceptions propagate up and are caught by tcpserver
159
+ end
160
+ end
161
+ end
162
+ abandon_all
163
+ end
164
+
165
+ # Start an operation in a Thread. Add this to a ThreadGroup to allow
166
+ # the operation to be abandoned later.
167
+ #
168
+ # When the thread terminates, it automatically drops out of the group.
169
+ #
170
+ # Note: RFC 2251 4.4.4.1 says behaviour is undefined if
171
+ # client sends an overlapping request with same message ID,
172
+ # so we don't have to worry about the case where there is
173
+ # already a thread with this messageId in @threadgroup.
174
+
175
+ def start_op(messageId,protocolOp,controls,meth)
176
+ operationClass = @opt[:operation_class]
177
+ ocArgs = @opt[:operation_args] || []
178
+ thr = Thread.new do
179
+ begin
180
+ operationClass.new(self,messageId,*ocArgs).
181
+ send(meth,protocolOp,controls)
182
+ rescue Exception => e
183
+ log_exception e
184
+ end
185
+ end
186
+ thr[:messageId] = messageId
187
+ @threadgroup.add(thr)
188
+ end
189
+
190
+ def write(data)
191
+ @mutex.synchronize do
192
+ @io.write(data)
193
+ @io.flush
194
+ end
195
+ end
196
+
197
+ def writelock
198
+ @mutex.synchronize do
199
+ yield @io
200
+ @io.flush
201
+ end
202
+ end
203
+
204
+ def abandon(messageID)
205
+ @mutex.synchronize do
206
+ thread = @threadgroup.list.find { |t| t[:messageId] == messageID }
207
+ thread.raise LDAP::Abandon if thread
208
+ end
209
+ end
210
+
211
+ def abandon_all
212
+ @mutex.synchronize do
213
+ @threadgroup.list.each do |thread|
214
+ thread.raise LDAP::Abandon
215
+ end
216
+ end
217
+ end
218
+
219
+ def send_unsolicited_notification(resultCode, opt={})
220
+ protocolOp = [
221
+ OpenSSL::ASN1::Enumerated(resultCode),
222
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
223
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
224
+ ]
225
+ if opt[:referral]
226
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
227
+ protocolOp << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
228
+ end
229
+ if opt[:responseName]
230
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
231
+ end
232
+ if opt[:response]
233
+ protocolOp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
234
+ end
235
+ message = [
236
+ OpenSSL::ASN1::Integer(0),
237
+ OpenSSL::ASN1::Sequence(protocolOp, 24, :IMPLICIT, :APPLICATION),
238
+ ]
239
+ message << opt[:controls] if opt[:controls]
240
+ write(OpenSSL::ASN1::Sequence(message).to_der)
241
+ end
242
+
243
+ def send_notice_of_disconnection(resultCode, errorMessage="")
244
+ send_unsolicited_notification(resultCode,
245
+ :errorMessage=>errorMessage,
246
+ :responseName=>"1.3.6.1.4.1.1466.20036"
247
+ )
248
+ end
249
+ end
250
+ end # class Server
251
+ 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