ruby-ldapserver 0.3.1

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