deveo-ruby-ldapserver 0.5.2

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,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