fakeldap 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +3 -0
  3. data/lib/fakeldap.rb +46 -0
  4. data/lib/fakeldap/version.rb +4 -0
  5. data/vendor/ruby-ldapserver/COPYING +27 -0
  6. data/vendor/ruby-ldapserver/ChangeLog +83 -0
  7. data/vendor/ruby-ldapserver/Manifest.txt +32 -0
  8. data/vendor/ruby-ldapserver/README +222 -0
  9. data/vendor/ruby-ldapserver/Rakefile +22 -0
  10. data/vendor/ruby-ldapserver/examples/README +89 -0
  11. data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
  12. data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
  13. data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
  14. data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
  15. data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
  16. data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
  17. data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
  18. data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
  19. data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
  20. data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
  21. data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
  22. data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
  23. data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
  24. data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
  25. data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
  26. data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
  27. data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
  28. data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
  29. data/vendor/ruby-ldapserver/test/core.schema +582 -0
  30. data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
  31. data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
  32. data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
  33. data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
  34. data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
  35. data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
  36. data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
  37. metadata +130 -0
@@ -0,0 +1,89 @@
1
+ require 'ldap/server/connection'
2
+ require 'ldap/server/operation'
3
+ require 'openssl'
4
+
5
+ module LDAP
6
+ class Server
7
+
8
+ attr_accessor :root_dse
9
+
10
+ DEFAULT_OPT = {
11
+ :port=>389,
12
+ :nodelay=>true,
13
+ }
14
+
15
+ # Create a new server. Options include all those to tcpserver/preforkserver
16
+ # plus:
17
+ # :operation_class=>Class - set Operation handler class
18
+ # :operation_args=>[...] - args to Operation.new
19
+ # :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL
20
+ # :ssl_ca_path=>directory - verify peer certificates
21
+ # :schema=>Schema - Schema object
22
+ # :namingContexts=>[dn, ...] - base DN(s) we answer
23
+
24
+ def initialize(opt = DEFAULT_OPT)
25
+ @opt = opt
26
+ @opt[:server] = self
27
+ @opt[:operation_class] ||= LDAP::Server::Operation
28
+ @opt[:operation_args] ||= []
29
+ LDAP::Server.ssl_prepare(@opt)
30
+ @schema = opt[:schema] # may be nil
31
+ @root_dse = Hash.new { |h,k| h[k] = [] }.merge({
32
+ 'objectClass' => ['top','openLDAProotDSE','extensibleObject'],
33
+ 'supportedLDAPVersion' => ['3'],
34
+ #'altServer' =>
35
+ #'supportedExtension' =>
36
+ #'supportedControl' =>
37
+ #'supportedSASLMechanisms' =>
38
+ })
39
+ @root_dse['subschemaSubentry'] = [@schema.subschema_dn] if @schema
40
+ @root_dse['namingContexts'] = opt[:namingContexts] if opt[:namingContexts]
41
+ end
42
+
43
+ # create opt[:ssl_ctx] from the other ssl options
44
+
45
+ def self.ssl_prepare(opt) # :nodoc:
46
+ if opt[:ssl_key_file] and opt[:ssl_cert_file]
47
+ ctx = OpenSSL::SSL::SSLContext.new
48
+ ctx.key = OpenSSL::PKey::RSA.new(File::read(opt[:ssl_key_file]))
49
+ ctx.cert = OpenSSL::X509::Certificate.new(File::read(opt[:ssl_cert_file]))
50
+ if opt[:ssl_ca_path]
51
+ ctx.ca_path = opt[:ssl_ca_path]
52
+ ctx.verify_mode =
53
+ OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
54
+ else
55
+ $stderr.puts "Warning: SSL peer certificate won't be verified"
56
+ end
57
+ opt[:ssl_ctx] = ctx
58
+ end
59
+ end
60
+
61
+ def run_tcpserver
62
+ require 'ldap/server/tcpserver'
63
+
64
+ opt = @opt
65
+ @thread = LDAP::Server.tcpserver(@opt) do
66
+ LDAP::Server::Connection::new(self,opt).handle_requests
67
+ end
68
+ end
69
+
70
+ def run_prefork
71
+ require 'ldap/server/preforkserver'
72
+
73
+ opt = @opt
74
+ @thread = LDAP::Server.preforkserver(@opt) do
75
+ LDAP::Server::Connection::new(self,opt).handle_requests
76
+ end
77
+ end
78
+
79
+ def join
80
+ @thread.join
81
+ end
82
+
83
+ def stop
84
+ @thread.raise Interrupt, "" # <= temporary fix for 1.8.6
85
+ @thread.join
86
+ end
87
+
88
+ end # class Server
89
+ end # module LDAP
@@ -0,0 +1,235 @@
1
+ module LDAP
2
+ class Server
3
+
4
+ # A class which describes LDAP SyntaxDescriptions. For now there is
5
+ # a global pool of Syntax objects (rather than each Schema object
6
+ # having its own set)
7
+
8
+ class Syntax
9
+ attr_reader :oid, :nhr, :binary, :desc
10
+
11
+ # Create a new Syntax object
12
+
13
+ def initialize(oid, desc=nil, opt={}, &blk)
14
+ @oid = oid
15
+ @desc = desc
16
+ @nhr = opt[:nhr] # not human-readable?
17
+ @binary = opt[:binary] # binary encoding forced?
18
+ @re = opt[:re] # regular expression for parsing
19
+ @def = nil
20
+ instance_eval(&blk) if blk
21
+ end
22
+
23
+ def to_s
24
+ @oid
25
+ end
26
+
27
+ # Create a new Syntax object, given its description string
28
+
29
+ def self.from_def(str, &blk)
30
+ m = LDAPSyntaxDescription.match(str)
31
+ raise LDAP::ResultError::InvalidAttributeSyntax,
32
+ "Bad SyntaxTypeDescription #{str.inspect}" unless m
33
+ new(m[1], m[2], :nhr=>(m[3] == 'TRUE'), :binary=>(m[4] == 'TRUE'), &blk)
34
+ end
35
+
36
+ # Convert this object to its description string
37
+
38
+ def to_def
39
+ return @def if @def
40
+ ans = "( #@oid "
41
+ ans << "DESC '#@desc' " if @desc
42
+ # These are OpenLDAP extensions
43
+ ans << "X-BINARY-TRANSFER-REQUIRED 'TRUE' " if @binary
44
+ ans << "X-NOT-HUMAN-READABLE 'TRUE' " if @nhr
45
+ ans << ")"
46
+ @def = ans
47
+ end
48
+
49
+ # Return true or a MatchData object if the given value is allowed
50
+ # by this syntax
51
+
52
+ def match(val)
53
+ return true if @re.nil?
54
+ @re.match(value_to_s(val))
55
+ end
56
+
57
+ # Convert a value for this syntax into its canonical string representation
58
+ # (not yet used, but seemed like a good idea)
59
+
60
+ def value_to_s(val)
61
+ val.to_s
62
+ end
63
+
64
+ # Convert a string value for this syntax into a Ruby-like value
65
+ # (not yet used, but seemed like a good idea)
66
+
67
+ def value_from_s(val)
68
+ val
69
+ end
70
+
71
+ @@syntaxes = {}
72
+
73
+ # Add a new syntax definition
74
+
75
+ def self.add(*args, &blk)
76
+ s = new(*args, &blk)
77
+ @@syntaxes[s.oid] = s
78
+ end
79
+
80
+ # Find a Syntax object given an oid. If not known, return a new empty
81
+ # Syntax object associated with this oid.
82
+
83
+ def self.find(oid)
84
+ return oid if oid.nil? or oid.is_a?(LDAP::Server::Syntax)
85
+ return @@syntaxes[oid] if @@syntaxes[oid]
86
+ add(oid)
87
+ end
88
+
89
+ # Return all known syntax objects
90
+
91
+ def self.all_syntaxes
92
+ @@syntaxes.values.uniq
93
+ end
94
+
95
+ # Shared constants for regexp-based syntax parsers
96
+
97
+ KEYSTR = "[a-zA-Z][a-zA-Z0-9;-]*"
98
+ NUMERICOID = "( \\d[\\d.]+\\d )"
99
+ WOID = "\\s* ( #{KEYSTR} | \\d[\\d.]+\\d ) \\s*"
100
+ _WOID = "\\s* (?: #{KEYSTR} | \\d[\\d.]+\\d ) \\s*"
101
+ OIDS = "( #{_WOID} | \\s* \\( #{_WOID} (?: \\$ #{_WOID} )* \\) \\s* )"
102
+ _QDESCR = "\\s* ' #{KEYSTR} ' \\s*"
103
+ QDESCRS = "( #{_QDESCR} | \\s* \\( (?:#{_QDESCR})+ \\) \\s* )"
104
+ QDSTRING = "\\s* ' (.*?) ' \\s*"
105
+ NOIDLEN = "(\\d[\\d.]+\\d) (?: \\{ (\\d+) \\} )?"
106
+ ATTRIBUTEUSAGE = "(userApplications|directoryOperation|distributedOperation|dSAOperation)"
107
+
108
+ end
109
+
110
+ class Syntax
111
+
112
+ # These are the 'SHOULD' support syntaxes from RFC2252 section 6
113
+
114
+ AttributeTypeDescription =
115
+ add("1.3.6.1.4.1.1466.115.121.1.3", "Attribute Type Description", :re=>
116
+ %r! \A \s* \( \s*
117
+ #{NUMERICOID} \s*
118
+ (?: NAME #{QDESCRS} )?
119
+ (?: DESC #{QDSTRING} )?
120
+ ( OBSOLETE \s* )?
121
+ (?: SUP #{WOID} )?
122
+ (?: EQUALITY #{WOID} )?
123
+ (?: ORDERING #{WOID} )?
124
+ (?: SUBSTR #{WOID} )?
125
+ (?: SYNTAX \s* #{NOIDLEN} \s* )? # capture 2
126
+ ( SINGLE-VALUE \s* )?
127
+ ( COLLECTIVE \s* )?
128
+ ( NO-USER-MODIFICATION \s* )?
129
+ (?: USAGE \s* #{ATTRIBUTEUSAGE} )?
130
+ \s* \) \s* \z !xu)
131
+
132
+ add("1.3.6.1.4.1.1466.115.121.1.5", "Binary", :nhr=>true)
133
+ # FIXME: value_to_s should BER-encode the value??
134
+
135
+ add("1.3.6.1.4.1.1466.115.121.1.6", "Bit String", :re=>/\A'([01]*)'B\z/)
136
+ # FIXME: convert to FixNum?
137
+
138
+ add("1.3.6.1.4.1.1466.115.121.1.7", "Boolean", :re=>/\A(TRUE|FALSE)\z/) do
139
+ def self.value_to_s(v)
140
+ return v if v.is_a?(string)
141
+ v ? "TRUE" : "FALSE"
142
+ end
143
+ def self.value_from_s(v)
144
+ v.upcase == "TRUE"
145
+ end
146
+ end
147
+
148
+ add("1.3.6.1.4.1.1466.115.121.1.8", "Certificate", :binary=>true, :nhr=>true)
149
+ add("1.3.6.1.4.1.1466.115.121.1.9", "Certificate List", :binary=>true, :nhr=>true)
150
+ add("1.3.6.1.4.1.1466.115.121.1.10", "Certificate Pair", :binary=>true, :nhr=>true)
151
+ add("1.3.6.1.4.1.1466.115.121.1.11", "Country String", :re=>/\A[A-Z]{2}\z/i)
152
+ add("1.3.6.1.4.1.1466.115.121.1.12", "Distinguished Name")
153
+ # FIXME: validate DN?
154
+ add("1.3.6.1.4.1.1466.115.121.1.15", "Directory String")
155
+ # missed due to lack of interest: "DIT Content Rule Description"
156
+ add("1.3.6.1.4.1.1466.115.121.1.22", "Facsimile Telephone Number")
157
+ add(" 1.3.6.1.4.1.1466.115.121.1.23", "Fax", :nhr=>true)
158
+ add("1.3.6.1.4.1.1466.115.121.1.24", "Generalized Time")
159
+ # FIXME: Validate Generalized Time (find X.208) and convert to/from Ruby Time
160
+ add("1.3.6.1.4.1.1466.115.121.1.26", "IA5 String")
161
+ add("1.3.6.1.4.1.1466.115.121.1.27", "Integer", :re=>/\A\d+\z/) do
162
+ def self.value_from_s(v)
163
+ v.to_i
164
+ end
165
+ end
166
+ add("1.3.6.1.4.1.1466.115.121.1.28", "JPEG", :nhr=>true)
167
+ MatchingRuleDescription =
168
+ add("1.3.6.1.4.1.1466.115.121.1.30", "Matching Rule Description", :re=>
169
+ %r! \A \s* \( \s*
170
+ #{NUMERICOID} \s*
171
+ (?: NAME #{QDESCRS} )?
172
+ (?: DESC #{QDSTRING} )?
173
+ ( OBSOLETE \s* )?
174
+ SYNTAX \s* #{NUMERICOID} \s*
175
+ \s* \) \s* \z !xu)
176
+ MatchingRuleUseDescription =
177
+ add("1.3.6.1.4.1.1466.115.121.1.31", "Matching Rule Use Description", :re=>
178
+ %r! \A \s* \( \s*
179
+ #{NUMERICOID} \s*
180
+ (?: NAME #{QDESCRS} )?
181
+ (?: DESC #{QDSTRING} )?
182
+ ( OBSOLETE \s* )?
183
+ APPLIES \s* #{OIDS} \s*
184
+ \s* \) \s* \z !xu)
185
+ add("1.3.6.1.4.1.1466.115.121.1.33", "MHS OR Address")
186
+ add("1.3.6.1.4.1.1466.115.121.1.34", "Name And Optional UID")
187
+ # missed due to lack of interest: "Name Form Description"
188
+ add("1.3.6.1.4.1.1466.115.121.1.36", "Numeric String", :re=>/\A\d+\z/)
189
+ ObjectClassDescription =
190
+ add("1.3.6.1.4.1.1466.115.121.1.37", "Object Class Description", :re=>
191
+ %r! \A \s* \( \s*
192
+ #{NUMERICOID} \s*
193
+ (?: NAME #{QDESCRS} )?
194
+ (?: DESC #{QDSTRING} )?
195
+ ( OBSOLETE \s* )?
196
+ (?: SUP #{OIDS} )?
197
+ (?: ( ABSTRACT|STRUCTURAL|AUXILIARY ) \s* )?
198
+ (?: MUST #{OIDS} )?
199
+ (?: MAY #{OIDS} )?
200
+ \s* \) \s* \z !xu)
201
+ add("1.3.6.1.4.1.1466.115.121.1.38", "OID", :re=>/\A#{WOID}\z/xu)
202
+ add("1.3.6.1.4.1.1466.115.121.1.39", "Other Mailbox")
203
+ add("1.3.6.1.4.1.1466.115.121.1.41", "Postal Address") do
204
+ def self.value_from_s(v)
205
+ v.split(/\$/)
206
+ end
207
+ def self.value_to_s(v)
208
+ return v.join("$") if v.is_a?(Array)
209
+ return v
210
+ end
211
+ end
212
+ add("1.3.6.1.4.1.1466.115.121.1.43", "Presentation Address")
213
+ add("1.3.6.1.4.1.1466.115.121.1.44", "Printable String")
214
+ add("1.3.6.1.4.1.1466.115.121.1.50", "Telephone Number")
215
+ add("1.3.6.1.4.1.1466.115.121.1.53", "UTC Time")
216
+
217
+ LDAPSyntaxDescription =
218
+ add("1.3.6.1.4.1.1466.115.121.1.54", "LDAP Syntax Description", :re=>
219
+ %r! \A \s* \( \s*
220
+ #{NUMERICOID} \s*
221
+ (?: DESC #{QDSTRING} )?
222
+ (?: X-BINARY-TRANSFER-REQUIRED \s* ' (TRUE|FALSE) ' \s* )?
223
+ (?: X-NOT-HUMAN-READABLE \s* ' (TRUE|FALSE) ' \s* )?
224
+ \s* \) \s* \z !xu)
225
+
226
+ # Missed due to lack of interest: "DIT Structure Rule Description"
227
+
228
+ # A few others from RFC2252 section 4.3.2
229
+ add("1.3.6.1.4.1.1466.115.121.1.4", "Audio", :nhr=>true)
230
+ add("1.3.6.1.4.1.1466.115.121.1.40", "Octet String")
231
+ add("1.3.6.1.4.1.1466.115.121.1.58", "Substring Assertion")
232
+ end
233
+
234
+ end # class Server
235
+ end # module LDAP
@@ -0,0 +1,91 @@
1
+ require 'socket'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ # Accept connections on a port, and for each one start a new thread
7
+ # and run the given block. Returns the Thread object for the listener.
8
+ #
9
+ # FIXME:
10
+ # - have a limit on total number of concurrent connects
11
+ # - have a limit on connections from a single IP, or from a /24
12
+ # (to avoid the trivial DoS that the first limit creates)
13
+ # - ACL using source IP address (or perhaps that belongs in application)
14
+ #
15
+ # Options:
16
+ # :port=>port number [required]
17
+ # :bindaddr=>"IP address"
18
+ # :user=>"username" - drop privileges after bind
19
+ # :group=>"groupname" - ditto
20
+ # :logger=>object - implements << method
21
+ # :listen=>number - listen queue depth
22
+ # :nodelay=>true - set TCP_NODELAY option
23
+
24
+ def self.tcpserver(opt, &blk)
25
+ logger = opt[:logger] || $stderr
26
+ server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
27
+
28
+ # Drop privileges if requested
29
+ require 'etc' if opt[:group] or opt[:user]
30
+ Process.gid = Process.egid = Etc.getgrnam(opt[:group]).gid if opt[:group]
31
+ Process.uid = Process.euid = Etc.getpwnam(opt[:user]).uid if opt[:user]
32
+
33
+ # Typically the O/S will buffer response data for 100ms before sending.
34
+ # If the response is sent as a single write() then there's no need for it.
35
+ if opt[:nodelay]
36
+ begin
37
+ server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
38
+ rescue Exception
39
+ end
40
+ end
41
+ # set queue size for incoming connections (default is 5)
42
+ server.listen(opt[:listen]) if opt[:listen]
43
+
44
+ Thread.new do
45
+ while true
46
+ begin
47
+ session = server.accept
48
+ # subtlety: copy 'session' into a block-local variable because
49
+ # it will change when the next session is accepted
50
+ Thread.new(session) do |s|
51
+ begin
52
+ s.instance_eval(&blk)
53
+ rescue Exception => e
54
+ logger << "[#{s.peeraddr[3]}]: #{e}: #{e.backtrace[0]}\n"
55
+ #logger << "[#{s.peeraddr[3]}]: #{e}: #{e.backtrace.join("\n\tfrom ")}\n"
56
+ ensure
57
+ s.close
58
+ end
59
+ end
60
+ rescue Interrupt
61
+ # This exception can be raised to shut the server down
62
+ server.close if server and not server.closed?
63
+ break
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ end # class Server
70
+ end # module LDAP
71
+
72
+ if __FILE__ == $0
73
+ # simple test
74
+ puts "Running a test POP3 server on port 1110"
75
+ t = LDAP::Server.tcpserver(:port=>1110) do
76
+ print "+OK I am a fake POP3 server\r\n"
77
+ while line = gets
78
+ case line
79
+ when /^quit/i
80
+ break
81
+ when /^crash/i
82
+ raise Errno::EPERM, "dammit!"
83
+ else
84
+ print "-ERR I don't understand #{line}"
85
+ end
86
+ end
87
+ print "+OK bye\r\n"
88
+ end
89
+ #sleep 10; t.raise Interrupt # uncomment to run for fixed time period
90
+ t.join
91
+ end
@@ -0,0 +1,88 @@
1
+ require 'ldap/server/result'
2
+
3
+ module LDAP
4
+ class Server
5
+
6
+ class Operation
7
+
8
+ # Return true if connection is not authenticated
9
+
10
+ def anonymous?
11
+ @connection.binddn.nil?
12
+ end
13
+
14
+ # Split dn string into its component parts, returning
15
+ # [ {attr=>val}, {attr=>val}, ... ]
16
+ #
17
+ # This is pretty horrible legacy stuff from X500; see RFC2253 for the
18
+ # full gore. It's stupid that the LDAP protocol sends the DN in string
19
+ # form, rather than in ASN1 form (as it does with search filters, for
20
+ # example), even though the DN syntax is defined in terms of ASN1!
21
+ #
22
+ # Attribute names are downcased, but values are not. For any
23
+ # case-insensitive attributes it's up to you to downcase them.
24
+ #
25
+ # Note that only v2 clients should add extra space around the comma.
26
+ # This is accepted, and so is semicolon instead of comma, but the
27
+ # full RFC1779 backwards-compatibility rules (e.g. quoted values)
28
+ # are not implemented.
29
+ #
30
+ # I *think* these functions will work correctly with UTF8-encoded
31
+ # characters, given that a multibyte UTF8 character does not contain
32
+ # the bytes 00-7F and therefore we cannot confuse '\', '+' etc
33
+
34
+ def self.split_dn(dn)
35
+ # convert \\ to \5c, \+ to \2b etc
36
+ dn2 = dn.gsub(/\\([ #,+"\\<>;])/) { "\\%02x" % $1[0] }
37
+
38
+ # Now we know that \\ and \, do not exist, it's safe to split
39
+ parts = dn2.split(/\s*[,;]\s*/)
40
+
41
+ parts.collect do |part|
42
+ res = {}
43
+
44
+ # Split each part into attr=val+attr=val
45
+ avs = part.split(/\+/)
46
+
47
+ avs.each do |av|
48
+ # These should all be of form attr=value
49
+ unless av =~ /^([^=]+)=(.*)$/
50
+ raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}"
51
+ end
52
+ attr, val = $1.downcase, $2
53
+ # Now we can decode those bits
54
+ attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
55
+ val.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
56
+ res[attr] = val
57
+ end
58
+ res
59
+ end
60
+ end
61
+
62
+ # Reverse of split_dn. Join [elements...]
63
+ # where each element can be {attr=>val,...} or [[attr,val],...]
64
+ # or just [attr,val]
65
+
66
+ def self.join_dn(elements)
67
+ dn = ""
68
+ elements.each do |elem|
69
+ av = ""
70
+ elem = [elem] if elem[0].is_a?(String)
71
+ elem.each do |attr,val|
72
+ av << "+" unless av == ""
73
+
74
+ av << attr << "=" <<
75
+ val.sub(/^([# ])/, '\\\\\\1').
76
+ sub(/( )$/, '\\\\\\1').
77
+ gsub(/([,+"\\<>;])/, '\\\\\\1')
78
+ end
79
+ dn << "," unless dn == ""
80
+ dn << av
81
+ end
82
+ dn
83
+ end
84
+
85
+ end # class Operation
86
+
87
+ end # class Server
88
+ end # module LDAP