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.
- data/COPYING +27 -0
- data/ChangeLog +83 -0
- data/Manifest.txt +32 -0
- data/README +222 -0
- data/Rakefile +22 -0
- data/examples/README +89 -0
- data/examples/mkcert.rb +31 -0
- data/examples/rbslapd1.rb +111 -0
- data/examples/rbslapd2.rb +161 -0
- data/examples/rbslapd3.rb +172 -0
- data/examples/speedtest.rb +37 -0
- data/lib/ldap/server.rb +4 -0
- data/lib/ldap/server/connection.rb +273 -0
- data/lib/ldap/server/filter.rb +223 -0
- data/lib/ldap/server/match.rb +283 -0
- data/lib/ldap/server/operation.rb +487 -0
- data/lib/ldap/server/preforkserver.rb +93 -0
- data/lib/ldap/server/result.rb +71 -0
- data/lib/ldap/server/schema.rb +592 -0
- data/lib/ldap/server/server.rb +89 -0
- data/lib/ldap/server/syntax.rb +235 -0
- data/lib/ldap/server/tcpserver.rb +91 -0
- data/lib/ldap/server/util.rb +88 -0
- data/lib/ldap/server/version.rb +11 -0
- data/test/core.schema +582 -0
- data/test/encoding_test.rb +279 -0
- data/test/filter_test.rb +107 -0
- data/test/match_test.rb +59 -0
- data/test/schema_test.rb +113 -0
- data/test/syntax_test.rb +40 -0
- data/test/test_helper.rb +2 -0
- data/test/util_test.rb +51 -0
- metadata +98 -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
|