ruby-ldapserver 0.5.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,11 +31,11 @@ class Server
31
31
  def log(msg, severity = Logger::INFO)
32
32
  @logger.add(severity, msg, @io.peeraddr[3])
33
33
  end
34
-
34
+
35
35
  def debug msg
36
36
  log msg, Logger::DEBUG
37
37
  end
38
-
38
+
39
39
  def log_exception(e)
40
40
  log "#{e}: #{e.backtrace.join("\n\tfrom ")}", Logger::ERROR
41
41
  end
@@ -93,8 +93,6 @@ class Server
93
93
  end
94
94
 
95
95
  def handle_requests
96
- operationClass = @opt[:operation_class]
97
- ocArgs = @opt[:operation_args] || []
98
96
  catch(:close) do
99
97
  while true
100
98
  begin
@@ -120,9 +118,14 @@ class Server
120
118
  case protocolOp.tag
121
119
  when 0 # BindRequest
122
120
  abandon_all
123
- @binddn, @version = operationClass.new(self,messageId,*ocArgs).
124
- do_bind(protocolOp, controls)
125
-
121
+ if @opt[:router]
122
+ @binddn, @version = @opt[:router].do_bind(self, messageId, protocolOp, controls)
123
+ else
124
+ operationClass = @opt[:operation_class]
125
+ ocArgs = @opt[:operation_args] || []
126
+ @binddn, @version = operationClass.new(self,messageId,*ocArgs).
127
+ do_bind(protocolOp, controls)
128
+ end
126
129
  when 2 # UnbindRequest
127
130
  throw(:close)
128
131
 
@@ -171,14 +174,17 @@ class Server
171
174
  # client sends an overlapping request with same message ID,
172
175
  # so we don't have to worry about the case where there is
173
176
  # already a thread with this messageId in @threadgroup.
174
-
175
177
  def start_op(messageId,protocolOp,controls,meth)
176
178
  operationClass = @opt[:operation_class]
177
179
  ocArgs = @opt[:operation_args] || []
178
180
  thr = Thread.new do
179
181
  begin
180
- operationClass.new(self,messageId,*ocArgs).
181
- send(meth,protocolOp,controls)
182
+ if @opt[:router]
183
+ @opt[:router].send meth, self, messageId, protocolOp, controls
184
+ else
185
+ operationClass.new(self,messageId,*ocArgs).
186
+ send(meth,protocolOp,controls)
187
+ end
182
188
  rescue Exception => e
183
189
  log_exception e
184
190
  end
@@ -0,0 +1,220 @@
1
+ require 'ldap/server/util'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ class DN
7
+ include Enumerable
8
+
9
+ attr_reader :dname
10
+
11
+ # Combines a set of elements to a syntactically correct DN
12
+ # elements is [elements, ...] where elements
13
+ # can be either { attr => val } or [attr, val]
14
+ def self.join(elements)
15
+ LDAP::Server::Operation.join_dn(elements)
16
+ end
17
+
18
+ def initialize(dn)
19
+ @dname = LDAP::Server::Operation.split_dn(dn)
20
+ end
21
+
22
+ # Returns the value of the first occurrence of attr (bottom-up)
23
+ def find_first(attr)
24
+ @dname.each do |pair|
25
+ return pair[attr.to_s] if pair[attr.to_s]
26
+ end
27
+ nil
28
+ end
29
+
30
+ # Returns the value of the last occurrence of attr (bottom-up)
31
+ def find_last(attr)
32
+ @dname.reverse_each do |pair|
33
+ return pair[attr.to_s] if pair[attr.to_s]
34
+ end
35
+ nil
36
+ end
37
+
38
+ # Returns all values of all occurrences of attr (bottom-up)
39
+ def find(attr)
40
+ result = []
41
+ @dname.each do |pair|
42
+ result << pair[attr.to_s] if pair[attr.to_s]
43
+ end
44
+ result
45
+ end
46
+
47
+ # Returns the value of the n-th occurrence of attr (top-down, 0 is first element)
48
+ def find_nth(attr, n)
49
+ i = 0
50
+ @dname.each do |pair|
51
+ if pair[attr.to_s]
52
+ return pair[attr.to_s] if i == n
53
+ i += 1
54
+ end
55
+ end
56
+ nil
57
+ end
58
+
59
+ # Whether or not the DN starts with dn (bottom-up)
60
+ # dn is a string
61
+ def start_with?(dn)
62
+ needle = LDAP::Server::Operation.split_dn(dn)
63
+
64
+ # Needle is longer than haystack
65
+ return false if needle.length > @dname.length
66
+
67
+ needle_index = 0
68
+ haystack_index = 0
69
+
70
+ while needle_index < needle.length
71
+ return false if @dname[haystack_index] != needle[needle_index]
72
+ needle_index += 1
73
+ haystack_index += 1
74
+ end
75
+ true
76
+ end
77
+
78
+ # Whether or not the DN starts with a format (bottom-up) (values are ignored)
79
+ # dn is a string
80
+ def start_with_format?(dn)
81
+ needle = LDAP::Server::Operation.split_dn(dn)
82
+
83
+ # Needle is longer than haystack
84
+ return false if needle.length > @dname.length
85
+
86
+ needle_index = 0
87
+ haystack_index = 0
88
+
89
+ while needle_index < needle.length
90
+ return false if @dname[haystack_index].keys != needle[needle_index].keys
91
+ needle_index += 1
92
+ haystack_index += 1
93
+ end
94
+ true
95
+ end
96
+
97
+ # Whether or not the DN ends with dn (top-down)
98
+ # dn is a string
99
+ def end_with?(dn)
100
+ needle = LDAP::Server::Operation.split_dn(dn)
101
+
102
+ # Needle is longer than haystack
103
+ return false if needle.length > @dname.length
104
+
105
+ needle_index = needle.length - 1
106
+ haystack_index = @dname.length - 1
107
+
108
+ while needle_index >= 0
109
+ return false if @dname[haystack_index] != needle[needle_index]
110
+ needle_index -= 1
111
+ haystack_index -= 1
112
+ end
113
+ true
114
+ end
115
+
116
+ # Whether or not the DN ends with format (top-down) (values are ignored)
117
+ # dn is a string
118
+ def end_with_format?(dn)
119
+ needle = LDAP::Server::Operation.split_dn(dn)
120
+
121
+ # Needle is longer than haystack
122
+ return false if needle.length > @dname.length
123
+
124
+ needle_index = needle.length - 1
125
+ haystack_index = @dname.length - 1
126
+
127
+ while needle_index >= 0
128
+ return false if @dname[haystack_index].keys != needle[needle_index].keys
129
+ needle_index -= 1
130
+ haystack_index -= 1
131
+ end
132
+ true
133
+ end
134
+
135
+ # Whether or not the DN equals dn (values are case sensitive)
136
+ # dn is a string
137
+ def equal?(dn)
138
+ split_dn = LDAP::Server::Operation.split_dn(dn)
139
+
140
+ return false if split_dn.length != @dname.length
141
+
142
+ @dname.each_with_index do |pair, index|
143
+ return false if pair != split_dn[index]
144
+ end
145
+ true
146
+ end
147
+
148
+ # Whether or not the DN equals dn's format (values are ignored) (case insensitive)
149
+ # dn is a string
150
+ def equal_format?(dn)
151
+ split_dn = LDAP::Server::Operation.split_dn(dn)
152
+
153
+ return false if split_dn.length != @dname.length
154
+
155
+ @dname.each_with_index do |pair, index|
156
+ return false if pair.keys != split_dn[index].keys
157
+ end
158
+ true
159
+ end
160
+
161
+ # Whether or not the DN constains a substring equal to dn (values are case sensitive)
162
+ # dn is a string
163
+ def include?(dn)
164
+ split_dn = LDAP::Server::Operation.split_dn(dn)
165
+ return false if split_dn.length > @dname.length
166
+ LDAP::Server::Operation.join_dn(@dname).include?(LDAP::Server::Operation.join_dn(split_dn))
167
+ end
168
+
169
+ # Whether or not the DN constains a substring format equal to dn (values are ignored) (case insensitive)
170
+ # dn is a string
171
+ def include_format?(dn)
172
+ split_dn = LDAP::Server::Operation.split_dn(dn)
173
+
174
+ return false if split_dn.length > @dname.length
175
+
176
+ haystack = []
177
+ @dname.each { |pair| haystack << pair.keys }
178
+
179
+ needle = []
180
+ split_dn.each { |pair| needle << pair.keys }
181
+
182
+ haystack.join.include?(needle.join)
183
+ end
184
+
185
+ # Generates a mapping for variables
186
+ # For example:
187
+ # > dn = LDAP::Server.DN.new("uid=user,ou=Users,dc=mydomain,dc=com")
188
+ # > dn.parse("uid=:uid, ou=:category, dc=mydomain, dc=com")
189
+ # => { :uid => "user", :category => "Users" }
190
+ def parse(template_dn)
191
+ result = {}
192
+ template = LDAP::Server::Operation.split_dn(template_dn)
193
+ template.reverse.zip(@dname.reverse).each do |temp, const|
194
+ break if const and temp.keys.first != const.keys.first
195
+ if temp.values.first.start_with?(':')
196
+ sym = temp.values.first[1..-1].to_sym
197
+ if const
198
+ result[sym] = const.values.first unless result[sym]
199
+ else
200
+ result[sym] = nil
201
+ end
202
+ elsif temp.values.first != const.values.first
203
+ break
204
+ end
205
+ end
206
+ result
207
+ end
208
+
209
+ def each(&block)
210
+ @dname.each(&block)
211
+ end
212
+
213
+ def reverse_each(&block)
214
+ @dname.reverse_each(&block)
215
+ end
216
+
217
+ end
218
+
219
+ end
220
+ end
@@ -202,7 +202,7 @@ class Server
202
202
  when :eq, :approx, :le, :ge, :substrings
203
203
  # the filter now includes a suitable matching object
204
204
  return (filter[2] || LDAP::Server::MatchingRule::DefaultMatch).send(
205
- filter.first, av[filter[1].to_s], *filter[3..-1])
205
+ filter.first, Array(av[filter[1].to_s]), *filter[3..-1])
206
206
 
207
207
  when :true
208
208
  return true
@@ -1,4 +1,5 @@
1
1
  require 'timeout'
2
+ require 'openssl'
2
3
  require 'ldap/server/result'
3
4
  require 'ldap/server/filter'
4
5
 
@@ -36,6 +37,7 @@ class Server
36
37
  ])
37
38
  @schema = @connection.opt[:schema]
38
39
  @server = @connection.opt[:server]
40
+ @attribute_range_limit = @connection.opt[:attribute_range_limit]
39
41
  end
40
42
 
41
43
  def log msg, severity = Logger::INFO
@@ -45,9 +47,9 @@ class Server
45
47
  def debug msg
46
48
  @connection.debug msg
47
49
  end
48
-
50
+
49
51
  # Send an exception report to the log
50
-
52
+
51
53
  def log_exception msg
52
54
  @connection.log_exception msg
53
55
  end
@@ -84,7 +86,7 @@ class Server
84
86
  seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
85
87
  end
86
88
  yield seq if block_given? # opportunity to add more elements
87
-
89
+
88
90
  send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
89
91
  end
90
92
 
@@ -96,6 +98,9 @@ class Server
96
98
  end
97
99
  end
98
100
 
101
+
102
+ AttributeRange = Struct.new :start, :end
103
+
99
104
  # Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
100
105
  # If schema given, return operational attributes only if
101
106
  # explicitly requested
@@ -114,23 +119,43 @@ class Server
114
119
  sendall = @attributes == [] || @attributes.include?("*")
115
120
  avseq = []
116
121
 
117
- avs.each do |attr, vals|
118
- if !@attributes.include?(attr)
122
+ avs.each_with_index do |(attr, vals), aidx|
123
+ query_attr_idx = @attributes.index(attr)
124
+ if !query_attr_idx
119
125
  next unless sendall
120
126
  if @schema
121
127
  a = @schema.find_attrtype(attr)
122
128
  next unless a and (a.usage.nil? or a.usage == :userApplications)
123
129
  end
124
130
  end
131
+ query_attr = query_attr_idx && @attribute_ranges[query_attr_idx]
125
132
 
126
133
  if @typesOnly
127
- vals = []
134
+ vals = []
128
135
  else
129
136
  vals = [vals] unless vals.kind_of?(Array)
130
137
  # FIXME: optionally do a value_to_s conversion here?
131
138
  # FIXME: handle attribute;binary
132
139
  end
133
140
 
141
+ if (@attribute_range_limit && vals.size > @attribute_range_limit) || query_attr&.start
142
+ if query_attr&.start
143
+ range_start = query_attr.start.to_i
144
+ range_end = query_attr.end == "*" ? -1 : query_attr.end.to_i
145
+ else
146
+ range_start = 0
147
+ range_end = @attribute_range_limit ? @attribute_range_limit - 1 : -1
148
+ end
149
+ range_end = range_start + @attribute_range_limit - 1 if @attribute_range_limit && (vals.size - range_start > @attribute_range_limit)
150
+ range_end = -1 if vals.size <= range_end
151
+ rvals = vals[range_start .. range_end]
152
+ vals = []
153
+ avseq << OpenSSL::ASN1::Sequence([
154
+ OpenSSL::ASN1::OctetString("#{attr};range=#{range_start}-#{range_end == -1 ? "*" : range_end}"),
155
+ OpenSSL::ASN1::Set(rvals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
156
+ ])
157
+ end
158
+
134
159
  avseq << OpenSSL::ASN1::Sequence([
135
160
  OpenSSL::ASN1::OctetString(attr),
136
161
  OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
@@ -200,8 +225,8 @@ class Server
200
225
  when 0
201
226
  simple_bind(version, dn, authentication.value)
202
227
  when 3
203
- mechanism = authentication.value[0].value
204
- credentials = authentication.value[1].value
228
+ # mechanism = authentication.value[0].value
229
+ # credentials = authentication.value[1].value
205
230
  # sasl_bind(version, dn, mechanism, credentials)
206
231
  # FIXME: needs to exchange further BindRequests
207
232
  raise LDAP::ResultError::AuthMethodNotSupported
@@ -246,7 +271,20 @@ class Server
246
271
  client_timelimit = protocolOp.value[4].value.to_i
247
272
  @typesOnly = protocolOp.value[5].value
248
273
  filter = Filter::parse(protocolOp.value[6], @schema)
249
- @attributes = protocolOp.value[7].value.collect {|x| x.value}
274
+ attributes = protocolOp.value[7].value.collect {|x| x.value}
275
+ attributes = attributes.map do |attr|
276
+ if attr =~ /(.*);range=(\d+)-(\d+|\*)\z/
277
+ [$1, $2, $3]
278
+ else
279
+ attr
280
+ end
281
+ end
282
+ @attributes = attributes.map do |name, |
283
+ name
284
+ end
285
+ @attribute_ranges = attributes.map do |_, range_start, range_end|
286
+ range_start && AttributeRange.new(range_start, range_end)
287
+ end
250
288
 
251
289
  @rescount = 0
252
290
  @sizelimit = server_sizelimit
@@ -0,0 +1,166 @@
1
+ require 'openssl'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ class Request
7
+ attr_accessor :connection, :typesOnly, :attributes, :rescount, :sizelimit
8
+
9
+ # Object to handle a single LDAP request. This object is created on
10
+ # every request by the router, and is passed as argument to the defined
11
+ # routes.
12
+
13
+ def initialize(connection, messageId)
14
+ @connection = connection
15
+ @respEnvelope = OpenSSL::ASN1::Sequence([
16
+ OpenSSL::ASN1::Integer(messageId),
17
+ # protocolOp,
18
+ # controls [0] OPTIONAL,
19
+ ])
20
+ @schema = @connection.opt[:schema]
21
+ @server = @connection.opt[:server]
22
+ @rescount = 0
23
+ end
24
+
25
+ ##################################################
26
+ ### Utility methods to send protocol responses ###
27
+ ##################################################
28
+
29
+ def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
30
+ @respEnvelope.value[1] = protocolOp
31
+ if opt[:controls]
32
+ @respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
33
+ else
34
+ @respEnvelope.value.delete_at(2)
35
+ end
36
+
37
+ @connection.write(@respEnvelope.to_der)
38
+ end
39
+
40
+ def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
41
+ seq = [
42
+ OpenSSL::ASN1::Enumerated(resultCode),
43
+ OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
44
+ OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
45
+ ]
46
+ if opt[:referral]
47
+ rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
48
+ seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
49
+ end
50
+ yield seq if block_given? # opportunity to add more elements
51
+
52
+ send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
53
+ end
54
+
55
+ def send_BindResponse(resultCode, opt={})
56
+ send_LDAPResult(1, resultCode, opt) do |resp|
57
+ if opt[:serverSaslCreds]
58
+ resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
64
+ # If schema given, return operational attributes only if
65
+ # explicitly requested
66
+
67
+ def send_SearchResultEntry(dn, avs, opt={})
68
+ @rescount += 1
69
+ if @sizelimit
70
+ raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
71
+ end
72
+
73
+ if @schema
74
+ # normalize the attribute names
75
+ @attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s }
76
+ end
77
+
78
+ sendall = @attributes == [] || @attributes.include?("*")
79
+ avseq = []
80
+
81
+ avs.each do |attr, vals|
82
+ if !@attributes.include?(attr)
83
+ next unless sendall
84
+ if @schema
85
+ a = @schema.find_attrtype(attr)
86
+ next unless a and (a.usage.nil? or a.usage == :userApplications)
87
+ end
88
+ end
89
+
90
+ if @typesOnly
91
+ vals = []
92
+ else
93
+ vals = [vals] unless vals.kind_of?(Array)
94
+ # FIXME: optionally do a value_to_s conversion here?
95
+ # FIXME: handle attribute;binary
96
+ end
97
+
98
+ avseq << OpenSSL::ASN1::Sequence([
99
+ OpenSSL::ASN1::OctetString(attr),
100
+ OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
101
+ ])
102
+ end
103
+
104
+ send_LDAPMessage(OpenSSL::ASN1::Sequence([
105
+ OpenSSL::ASN1::OctetString(dn),
106
+ OpenSSL::ASN1::Sequence(avseq),
107
+ ], 4, :IMPLICIT, :APPLICATION), opt)
108
+ end
109
+
110
+ def send_SearchResultDone(resultCode, opt={})
111
+ send_LDAPResult(5, resultCode, opt)
112
+ end
113
+
114
+ def send_ModifyResponse(resultCode, opt={})
115
+ send_LDAPResult(7, resultCode, opt)
116
+ end
117
+
118
+ def send_AddResponse(resultCode, opt={})
119
+ send_LDAPResult(9, resultCode, opt)
120
+ end
121
+
122
+ def send_DelResponse(resultCode, opt={})
123
+ send_LDAPResult(11, resultCode, opt)
124
+ end
125
+
126
+ def send_ModifyDNResponse(resultCode, opt={})
127
+ send_LDAPResult(13, resultCode, opt)
128
+ end
129
+
130
+ def send_CompareResponse(resultCode, opt={})
131
+ send_LDAPResult(15, resultCode, opt)
132
+ end
133
+
134
+ def send_ExtendedResponse(resultCode, opt={})
135
+ send_LDAPResult(24, resultCode, opt) do |resp|
136
+ if opt[:responseName]
137
+ resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
138
+ end
139
+ if opt[:response]
140
+ resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
141
+ end
142
+ end
143
+ end
144
+
145
+ ############################################################
146
+ ### Methods to get parameters related to this connection ###
147
+ ############################################################
148
+
149
+ # Server-set maximum time limit. Override for more complex behaviour
150
+ # (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
151
+
152
+ def server_timelimit
153
+ @connection.opt[:timelimit]
154
+ end
155
+
156
+ # Server-set maximum size limit. Override for more complex behaviour
157
+ # (e.g. limit depends on @connection.binddn). Return nil for unlimited.
158
+
159
+ def server_sizelimit
160
+ @connection.opt[:sizelimit]
161
+ end
162
+
163
+ end
164
+
165
+ end
166
+ end
@@ -63,7 +63,7 @@ class ResultError
63
63
  53 => UnwillingToPerform,
64
64
  # FIXME: please fill in the rest
65
65
  }
66
- def self.[] (n)
66
+ def self.[](n)
67
67
  return N_TO_CLASS[n] || self
68
68
  end
69
69
  end # class ResultError