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,104 @@
1
+ require 'ldap/server/connection'
2
+ require 'ldap/server/operation'
3
+ require 'openssl'
4
+ require 'logger'
5
+
6
+ module LDAP
7
+ class Server
8
+
9
+ attr_accessor :root_dse
10
+
11
+ DEFAULT_OPT = {
12
+ :port=>389,
13
+ :nodelay=>true,
14
+ }
15
+
16
+ # Create a new server. Options include all those to tcpserver/preforkserver
17
+ # plus:
18
+ # :operation_class=>Class - set Operation handler class
19
+ # :operation_args=>[...] - args to Operation.new
20
+ # :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL
21
+ # :ssl_ca_path=>directory - verify peer certificates
22
+ # :schema=>Schema - Schema object
23
+ # :namingContexts=>[dn, ...] - base DN(s) we answer
24
+
25
+ attr_reader :logger
26
+
27
+ def initialize(opt = DEFAULT_OPT)
28
+ @opt = opt
29
+ @opt[:server] = self
30
+ @opt[:operation_class] ||= LDAP::Server::Operation
31
+ @opt[:operation_args] ||= []
32
+ unless @opt[:logger]
33
+ @opt[:logger] ||= Logger.new($stderr)
34
+ @opt[:logger].level = Logger::INFO
35
+ end
36
+ @logger = @opt[:logger]
37
+ LDAP::Server.ssl_prepare(@opt)
38
+ @schema = opt[:schema] # may be nil
39
+ @root_dse = Hash.new { |h,k| h[k] = [] }.merge({
40
+ 'objectClass' => ['top','openLDAProotDSE','extensibleObject'],
41
+ 'supportedLDAPVersion' => ['3'],
42
+ #'altServer' =>
43
+ #'supportedExtension' =>
44
+ #'supportedControl' =>
45
+ #'supportedSASLMechanisms' =>
46
+ })
47
+ @root_dse['subschemaSubentry'] = [@schema.subschema_dn] if @schema
48
+ @root_dse['namingContexts'] = opt[:namingContexts] if opt[:namingContexts]
49
+ end
50
+
51
+ # create opt[:ssl_ctx] from the other ssl options
52
+
53
+ def self.ssl_prepare(opt) # :nodoc:
54
+ if opt[:ssl_key_file] and opt[:ssl_cert_file]
55
+ ctx = OpenSSL::SSL::SSLContext.new
56
+ ctx.key = OpenSSL::PKey::RSA.new(File::read(opt[:ssl_key_file]))
57
+ ctx.cert = OpenSSL::X509::Certificate.new(File::read(opt[:ssl_cert_file]))
58
+ if opt[:ssl_dhparams]
59
+ ctx.tmp_dh_callback = proc { |*args|
60
+ OpenSSL::PKey::DH.new(
61
+ File.read(opt[:ssl_dhparams])
62
+ )
63
+ }
64
+ end
65
+ if opt[:ssl_ca_path]
66
+ ctx.ca_path = opt[:ssl_ca_path]
67
+ ctx.verify_mode = opt[:ssl_verify_mode] ||
68
+ OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
69
+ elsif opt[:ssl_verify_mode] != 0
70
+ $stderr.puts "Warning: No ssl_ca_path, peer certificate won't be verified"
71
+ end
72
+ opt[:ssl_ctx] = ctx
73
+ end
74
+ end
75
+
76
+ def run_tcpserver
77
+ require 'ldap/server/tcpserver'
78
+
79
+ opt = @opt
80
+ @thread = LDAP::Server.tcpserver(@opt) do
81
+ LDAP::Server::Connection::new(self,opt).handle_requests
82
+ end
83
+ end
84
+
85
+ def run_prefork
86
+ require 'ldap/server/preforkserver'
87
+
88
+ opt = @opt
89
+ @thread = LDAP::Server.preforkserver(@opt) do
90
+ LDAP::Server::Connection::new(self,opt).handle_requests
91
+ end
92
+ end
93
+
94
+ def join
95
+ @thread.join
96
+ end
97
+
98
+ def stop
99
+ @thread.raise Interrupt, "" # <= temporary fix for 1.8.6
100
+ @thread.join
101
+ end
102
+
103
+ end # class Server
104
+ 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,89 @@
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
+ server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
26
+
27
+ # Drop privileges if requested
28
+ require 'etc' if opt[:group] or opt[:user]
29
+ Process.gid = Process.egid = Etc.getgrnam(opt[:group]).gid if opt[:group]
30
+ Process.uid = Process.euid = Etc.getpwnam(opt[:user]).uid if opt[:user]
31
+
32
+ # Typically the O/S will buffer response data for 100ms before sending.
33
+ # If the response is sent as a single write() then there's no need for it.
34
+ if opt[:nodelay]
35
+ begin
36
+ server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
37
+ rescue Exception
38
+ end
39
+ end
40
+ # set queue size for incoming connections (default is 5)
41
+ server.listen(opt[:listen]) if opt[:listen]
42
+
43
+ Thread.new do
44
+ while true
45
+ begin
46
+ session = server.accept
47
+ # subtlety: copy 'session' into a block-local variable because
48
+ # it will change when the next session is accepted
49
+ Thread.new(session) do |s|
50
+ begin
51
+ s.instance_eval(&blk)
52
+ rescue Exception => e
53
+ opt[:logger].error(s.peeraddr[3]) {"#{e}: #{e.backtrace[0]}"}
54
+ ensure
55
+ s.close
56
+ end
57
+ end
58
+ rescue Interrupt
59
+ # This exception can be raised to shut the server down
60
+ server.close if server and not server.closed?
61
+ break
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ end # class Server
68
+ end # module LDAP
69
+
70
+ if __FILE__ == $0
71
+ # simple test
72
+ puts "Running a test POP3 server on port 1110"
73
+ t = LDAP::Server.tcpserver(:port=>1110) do
74
+ print "+OK I am a fake POP3 server\r\n"
75
+ while line = gets
76
+ case line
77
+ when /^quit/i
78
+ break
79
+ when /^crash/i
80
+ raise Errno::EPERM, "dammit!"
81
+ else
82
+ print "-ERR I don't understand #{line}"
83
+ end
84
+ end
85
+ print "+OK bye\r\n"
86
+ end
87
+ #sleep 10; t.raise Interrupt # uncomment to run for fixed time period
88
+ t.join
89
+ 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].ord }
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