This commit is contained in:
pvincent
2026-06-05 11:20:14 +00:00
commit 79d31f61c4
50 changed files with 6127 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
Using the example programs
==========================
These servers all listen on port 1389 by default, so that they don't have to
be run as root.
Example 1: trivial server using RAM hash
----------------------------------------
$ ruby rbslapd1.rb
In another window:
$ ldapadd -x -H ldap://127.0.0.1:1389/
dn: dc=example,dc=com
cn: Top object
dn: cn=Fred Flintstone,dc=example,dc=com
cn: Fred Flintstone
sn: Flintstone
mail: fred@bedrock.org
mail: fred.flintstone@bedrock.org
dn: cn=Wilma Flintstone,dc=example,dc=com
cn: Wilma Flintstone
mail: wilma@bedrock.org
^D
Try these queries:
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "" "(objectclass=*)"
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" -s base "(objectclass=*)"
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(mail=fred*)"
If you terminate the server with Ctrl-C, its contents should be written
to disk as a YAML file.
A fairly complete set of the filter language is implemented. However, this
simple server works by simply scanning the entire database and applying the
filter to each entry, so it won't scale to large applications. No validation
of DN or attributes against any sort of schema is done.
Example 1a: with SSL
--------------------
In rbslapd1.rb, uncomment
:ssl_key_file => "key.pem",
:ssl_cert_file => "cert.pem",
:ssl_on_connect => true,
and run mkcert.rb. Since this is a self-signed certificate, you'll have to
turn off certificate verification in the client too. For example:
$ env LDAPTLS_REQCERT="allow" ldapsearch -x -H ldaps://127.0.0.1:1389/
Making your own CA and installing its certificate in the client, or
generating a Certificate Signing Request and sending it to a known CA, is
beyond the scope of this documentation.
Example 2: simple LDAP to SQL mapping
-------------------------------------
You will need to set up a MySQL database with a table conforming to the
schema given within the code. Once done, LDAP gives a read-only view of the
database with only the filter "(uid=<foo>)" supported.
Example 3: preforking server and schema
---------------------------------------
This functions in the same way as rbslapd1.rb. However, since each query is
answered in a separate process, the YAML file on disk is used as the master
repository. Update operations re-write this file each time.
Also, the schema is read from file 'core.schema'. Attempting to insert the
above entries will fail, due to schema violations. Insert a valid entry,
e.g.
dn: cn=Fred Flintstone,dc=example,dc=com
objectClass: organizationalPerson
cn: Fred Flintstone
sn: Flintstone
telephoneNumber: +1 555 1234
telephoneNumber: +1 555 5432
Schema validation takes place for the attribute values and that attributes
are allowed/required by the objectclass(es); however, the DN itself is not
validated, nor any checks made that the RDN is present as an attribute
(since this is one of the more stupid parts of the LDAP/X500 data model)
Example 4
------------
* ruby rbslapd4.rb
* `ldapwhoami -x -H ldap://127.0.0.1:1389 -D "uid=admin,ou=Users,dc=mydomain,dc=com" -w "adminpassword"`
+32
View File
@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFhTCCA22gAwIBAgIBADANBgkqhkiG9w0BAQUFADAwMQswCQYDVQQGEwJKUDEN
MAsGA1UECgwEVEVTVDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDYwNTEwMTc1
MloXDTI2MDYwNTExMTc1MlowMDELMAkGA1UEBhMCSlAxDTALBgNVBAoMBFRFU1Qx
EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBALNUH3qAAd15GiX+69MxJPzLL58CgDvBUKZsMzoxlJZw27UQywcb7PM5uyir
DHX0f6+JbV5uDNQ3iNdnqVeQChd32C6OXMo5lzKCl7zLxrrdGrizgN63dcXjLzpA
WDv1NjVVc8eI/Bs1lMEjG+lZRN1yqF05yxjBo+IJbb5rHgcHoKclwxrzs9wrc1Ff
ZnM9dMbytEUrZgssq+NAmBm6ie7XGdTSwdANKUJqMD7IUht+dqoFM5TlpgFUBkyf
jRXpsBgBkab+87o6OZk9mKIBM09ITgJmKhjGiUZeQ6VLLPx5J+WBicPdmg6kfyXO
OpA7QReNZz3ODRyy2ksZ8upsNxsm9R1TeWLYGhfRuTnlBiq8x8EDB00vvhXO5anf
hwwJKiFjx7BI90VpfVd48KXSYI5p+fkXWjrteT7P2yN9Fxv6k70Mhy62FZQo+Q34
QWRsFYg4UrMWpPZUwtwqfXaeq/d9eBXvropotJASXHlFLUsHLjyG6Bmj/qfBMcJW
cti5nReDu4oIFr2xXND746Kx6zEwzR/6tAaimZ1BNYwqCCPxoaYpjC4WnXMiUVF8
78THoIzIrxW9H9ofE4y1bJQJ6F6KZAywOMoC/5dM8xfYePyIFpmR0k2pVKiTO/Yc
fyVWKLYk67jMg35e5zj91wmw0maVJZWNutVYnKUImfb5P+X5AgMBAAGjgakwgaYw
CQYDVR0TBAIwADAdBgNVHQ4EFgQUg2LjMxC59ERv/MY+n9WOtSPn/6gwEwYDVR0l
BAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgSwMFgGA1UdIwRRME+AFINi4zMQufRE
b/zGPp/VjrUj5/+ooTSkMjAwMQswCQYDVQQGEwJKUDENMAsGA1UECgwEVEVTVDES
MBAGA1UEAwwJbG9jYWxob3N0ggEAMA0GCSqGSIb3DQEBBQUAA4ICAQCYeNO7TZ64
QQUBPvu75sYhhOBwEtcQMR/lHNYoqYy0TA4W/E+wwiRPweaMkPyUkzkK2/NZXP7w
QB+gT4rMszN2fPZi6Bvr2M0QtO8/YVEzMPs5Y7XcJqL8TtRsUrNYUTLtLoZ4iebq
G7hsFNwQLAdSQ+/xd+LfcxGNjXmmErhbbR5B1WsVJ5tYmW4qTuYQpZF5lOqQKEfh
98ea9eJNTgWVk14Hk1cwsuZaH4IcDQgPhmu7fMvyunShk4a4ArDNRNE656w2uZch
ThViRVdh+bqew76PS5zQHlGNJDgySYxTIVLhlWwSoFHLZkal6W8IqkKfurFA9dbI
Qi15eoqxwApXFniq10Y1f82lOiW3IJNXPhEh7ch8mgOQ/uDppAf4HmkIy3CguMr3
F0pWVs8a7zliOw2Ejj8L2hLel64KUhmCQWfoD7gFdnj1jpZr2tlv/GBbpWhpt2X2
8Ok9bHiMXvmUFd/g8rG2XadkiFGwOTC0OguD/7tT8BmNRWDJb8Ga3dDnVzQ4xmV+
Q3M8f8jYxw0Hrs8cW9xyW+gSckeGLkKrtoqhS/DNaNzdk12MZ5wwSgyuvZjj9h84
wuF8+3KhE6ucdrr5cuqzAkaeGPe6fvEfkllSLt5s6gmN+6/bTg1/f1l5o/xWYlUH
Q34692xrylRsx0qRw37Rk525ihmim+LLlw==
-----END CERTIFICATE-----
+51
View File
@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
+17
View File
@@ -0,0 +1,17 @@
---
dc=example,dc=com:
cn:
- Top object
cn=fred flintstone,dc=example,dc=com:
cn:
- Fred Flintstone
sn:
- Flintstone
mail:
- fred@bedrock.org
- fred.flintstone@bedrock.org
cn=wilma flintstone,dc=example,dc=com:
cn:
- Wilma Flintstone
mail:
- wilma@bedrock.org
+34
View File
@@ -0,0 +1,34 @@
require 'openssl'
# Taken directly from echo_svr.rb in the Ruby openssl examples
key = OpenSSL::PKey::RSA.new(4096) do
print '.'
$stdout.flush
end
puts
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
name = OpenSSL::X509::Name.new([%w[C JP], %w[O TEST], %w[CN localhost]])
cert.subject = name
cert.issuer = name
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.public_key = key.public_key
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
cert.extensions = [
ef.create_extension('basicConstraints', 'CA:FALSE'),
ef.create_extension('subjectKeyIdentifier', 'hash'),
ef.create_extension('extendedKeyUsage', 'serverAuth'),
ef.create_extension('keyUsage',
'keyEncipherment,dataEncipherment,digitalSignature')
]
ef.issuer_certificate = cert
cert.add_extension ef.create_extension('authorityKeyIdentifier',
'keyid:always,issuer:always')
cert.sign(key, OpenSSL::Digest.new('SHA1'))
# Write to disk
File.open('key.pem', 'w', 0o600) { |f| f << key.to_pem }
File.open('cert.pem', 'w', 0o644) { |f| f << cert.to_pem }
+112
View File
@@ -0,0 +1,112 @@
#!/usr/local/bin/ruby -w
# This is a trivial LDAP server which just stores directory entries in RAM.
# It does no validation or authentication. This is intended just to
# demonstrate the API, it's not for real-world use!!
$:.unshift('../lib')
$debug = true
require 'ldap/server'
# We subclass the Operation class, overriding the methods to do what we need
class HashOperation < LDAP::Server::Operation
def initialize(connection, messageID, hash)
super(connection, messageID)
@hash = hash # an object reference to our directory data
end
def search(basedn, scope, deref, filter)
basedn = basedn.downcase
case scope
when LDAP::Server::BaseObject
# client asked for single object by DN
obj = @hash[basedn]
raise LDAP::ResultError::NoSuchObject unless obj
send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj)
when LDAP::Server::WholeSubtree
@hash.each do |dn, av|
next unless dn.index(basedn, -basedn.length) # under basedn?
next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
send_SearchResultEntry(dn, av)
end
else
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
end
end
def add(dn, av)
dn = dn.downcase
raise LDAP::ResultError::EntryAlreadyExists if @hash[dn]
@hash[dn] = av
end
def del(dn)
dn = dn.downcase
raise LDAP::ResultError::NoSuchObject unless @hash.has_key?(dn)
@hash.delete(dn)
end
def modify(dn, ops)
dn = dn.downcase
entry = @hash[dn]
raise LDAP::ResultError::NoSuchObject unless entry
ops.each do |attr, vals|
op = vals.shift
case op
when :add
entry[attr] ||= []
entry[attr] += vals
entry[attr].uniq!
when :delete
if vals == []
entry.delete(attr)
else
vals.each { |v| entry[attr].delete(v) }
end
when :replace
entry[attr] = vals
end
entry.delete(attr) if entry[attr] == []
end
end
end
# This is the shared object which carries our actual directory entries.
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
directory = {}
# Let's put some backing store on it
require 'yaml'
begin
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
rescue Errno::ENOENT
end
at_exit do
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
File.rename("ldapdb.new","ldapdb.yaml")
end
# Listen for incoming LDAP connections. For each one, create a Connection
# object, which will invoke a HashOperation object for each request.
s = LDAP::Server.new(
:port => 1389,
:nodelay => true,
:listen => 10,
# :ssl_key_file => "key.pem",
# :ssl_cert_file => "cert.pem",
# :ssl_on_connect => true,
:operation_class => HashOperation,
:operation_args => [directory]
)
s.run_tcpserver
s.join
+161
View File
@@ -0,0 +1,161 @@
#!/usr/local/bin/ruby -w
$:.unshift('../lib')
require 'ldap/server'
require 'mysql' # <http://www.tmtm.org/en/ruby/mysql/>
require 'thread'
require 'resolv-replace' # ruby threading DNS client
# An example of an LDAP to SQL gateway. We have a MySQL table which
# contains (login_id,login,passwd) combinations, e.g.
#
# +----------+----------+--------+
# | login_id | login | passwd |
# +----------+----------+--------+
# | 1 | brian | foobar |
# | 2 | caroline | boing |
# +----------+----------+--------+
#
# We support LDAP searches for (uid=login), returning a synthesised DN and
# Maildir attribute, and we support LDAP binds to validate passwords. We
# keep a cache of recent lookups so that a bind to validate a password
# doesn't cause a second SQL query. Since we're multi-threaded, this should
# work even if the bind occurs on a different client connection to the search.
#
# To test:
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(uid=brian)"
#
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" \
# -D "id=1,dc=example,dc=com" -W "(uid=brian)"
$debug = true
SQL_CONNECT = ["1.2.3.4", "myuser", "mypass", "mydb"]
TABLE = "logins"
SQL_POOL_SIZE = 5
PW_CACHE_SIZE = 100
BASEDN = "dc=example,dc=com"
LDAP_PORT = 1389
# A thread-safe pool of persistent MySQL connections
class SQLPool
def initialize(n, *args)
@args = args
@pool = Queue.new # this is a thread-safe queue
n.times { @pool.push nil } # create connections on demand
end
def borrow
conn = @pool.pop || Mysql::new(*@args)
yield conn
rescue Exception
conn = nil # put 'nil' back into the pool
raise
ensure
@pool.push conn
end
end
# An simple LRU cache of username->password. It's linearly searched
# so don't make it too big.
class LRUCache
def initialize(size)
@size = size
@cache = [] # [[key,val],[key,val],...]
@mutex = Mutex.new
end
def add(id,data)
@mutex.synchronize do
@cache.delete_if { |k,v| k == id }
@cache.unshift [id,data]
@cache.pop while @cache.size > @size
end
end
def find(id)
@mutex.synchronize do
index = entry = nil
@cache.each_with_index do |e, i|
if e[0] == id
entry = e
index = i
break
end
end
return nil unless index
@cache.delete_at(index)
@cache.unshift entry
return entry[1]
end
end
end
class SQLOperation < LDAP::Server::Operation
def self.setcache(cache,pool)
@@cache = cache
@@pool = pool
end
# Handle searches of the form "(uid=<foo>)" using SQL backend
# (uid=foo) => [:eq, "uid", matchobj, "foo"]
def search(basedn, scope, deref, filter)
raise LDAP::ResultError::UnwillingToPerform, "Bad base DN" unless basedn == BASEDN
raise LDAP::ResultError::UnwillingToPerform, "Bad filter" unless filter[0..1] == [:eq, "uid"]
uid = filter[3]
@@pool.borrow do |sql|
q = "select login_id,passwd from #{TABLE} where login='#{sql.quote(uid)}'"
puts "SQL Query #{sql.object_id}: #{q}" if $debug
res = sql.query(q)
res.each do |login_id,passwd|
@@cache.add(login_id, passwd)
send_SearchResultEntry("id=#{login_id},#{BASEDN}", {
"maildir"=>["/netapp/#{uid}/"],
})
end
end
end
# Validate passwords
def simple_bind(version, dn, password)
return if dn.nil? # accept anonymous
raise LDAP::ResultError::UnwillingToPerform unless dn =~ /\Aid=(\d+),#{BASEDN}\z/
login_id = $1
dbpw = @@cache.find(login_id)
unless dbpw
@@pool.borrow do |sql|
q = "select passwd from #{TABLE} where login_id=#{login_id}"
puts "SQL Query #{sql.object_id}: #{q}" if $debug
res = sql.query(q)
if res.num_rows == 1
dbpw = res.fetch_row[0]
@@cache.add(login_id, dbpw)
end
end
end
raise LDAP::ResultError::InvalidCredentials unless dbpw and dbpw != "" and dbpw == password
end
end
# Build the objects we need
cache = LRUCache.new(PW_CACHE_SIZE)
pool = SQLPool.new(SQL_POOL_SIZE, *SQL_CONNECT)
SQLOperation.setcache(cache,pool)
s = LDAP::Server.new(
:port => LDAP_PORT,
:nodelay => true,
:listen => 10,
# :ssl_key_file => "key.pem",
# :ssl_cert_file => "cert.pem",
# :ssl_on_connect => true,
:operation_class => SQLOperation
)
s.run_tcpserver
s.join
+11
View File
@@ -0,0 +1,11 @@
CREATE TABLE logins (
login_id MEDIUMINT NOT NULL AUTO_INCREMENT,
login CHAR(30) NOT NULL,
passwd CHAR(30),
PRIMARY KEY (login_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO logins(login, passwd) VALUES
('brian', 'foobar'), ('caroline', 'boing');
SELECT * FROM logins;
+172
View File
@@ -0,0 +1,172 @@
#!/usr/local/bin/ruby -w
# This is similar to rbslapd1.rb but here we use TOMITA Masahiro's prefork
# library: <http://raa.ruby-lang.org/project/prefork/>
# Advantages over Ruby threading:
# - each client connection is handled in its own process; don't need
# to worry about Ruby thread blocking (except if one client issues
# overlapping LDAP operations down the same connection, which is uncommon)
# - better scalability on multi-processor systems
# - better scalability on single-processor systems (e.g. shouldn't hit
# max FDs per process limit)
# Disadvantages:
# - client connections can't share state in RAM. So our shared directory
# now has to be read from disk, and flushed to disk after every update.
#
# Additionally, I have added schema support. An LDAP v3 client can
# query the schema remotely, and adds/modifies have data validated.
$:.unshift('../lib')
require 'ldap/server'
require 'ldap/server/schema'
require 'yaml'
$debug = nil # $stderr
# An object to keep our in-RAM database and synchronise it to disk
# when necessary
class Directory
attr_reader :data
def initialize(filename)
@filename = filename
@stat = nil
update
end
# synchronise with directory on disk (re-read if it has changed)
def update
begin
tmp = {}
sb = File.stat(@filename)
return if @stat and @stat.ino == sb.ino and @stat.mtime == sb.mtime
File.open(@filename) do |f|
tmp = YAML::load(f.read)
@stat = f.stat
end
rescue Errno::ENOENT
end
@data = tmp
end
# write back to disk
def write
File.open(@filename+".new","w") { |f| f.write(YAML::dump(@data)) }
File.rename(@filename+".new",@filename)
@stat = File.stat(@filename)
end
# run a block while holding a lock on the database
def lock
File.open(@filename+".lock","w") do |f|
f.flock(File::LOCK_EX) # will block here until lock available
yield
end
end
end
# We subclass the Operation class, overriding the methods to do what we need
class DirOperation < LDAP::Server::Operation
def initialize(connection, messageID, dir)
super(connection, messageID)
@dir = dir
end
def search(basedn, scope, deref, filter)
$debug << "Search: basedn=#{basedn.inspect}, scope=#{scope.inspect}, deref=#{deref.inspect}, filter=#{filter.inspect}\n" if $debug
basedn = basedn.downcase
case scope
when LDAP::Server::BaseObject
# client asked for single object by DN
@dir.update
obj = @dir.data[basedn]
raise LDAP::ResultError::NoSuchObject unless obj
ok = LDAP::Server::Filter.run(filter, obj)
$debug << "Match=#{ok.inspect}: #{obj.inspect}\n" if $debug
send_SearchResultEntry(basedn, obj) if ok
when LDAP::Server::WholeSubtree
@dir.update
@dir.data.each do |dn, av|
$debug << "Considering #{dn}\n" if $debug
next unless dn.index(basedn, -basedn.length) # under basedn?
next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
$debug << "Sending: #{av.inspect}\n" if $debug
send_SearchResultEntry(dn, av)
end
else
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
end
end
def add(dn, entry)
entry = @schema.validate(entry)
entry['createTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
entry['creatorsName'] = [@connection.binddn.to_s]
# FIXME: normalize the DN and check it's below our root DN
# FIXME: validate that a superior object exists
# FIXME: validate that entry contains the RDN attribute (yuk)
dn = dn.downcase
@dir.lock do
@dir.update
raise LDAP::ResultError::EntryAlreadyExists if @dir.data[dn]
@dir.data[dn] = entry
@dir.write
end
end
def del(dn)
dn = dn.downcase
@dir.lock do
@dir.update
raise LDAP::ResultError::NoSuchObject unless @dir.data.has_key?(dn)
@dir.data.delete(dn)
@dir.write
end
end
def modify(dn, ops)
dn = dn.downcase
@dir.lock do
@dir.update
entry = @dir.data[dn]
raise LDAP::ResultError::NoSuchObject unless entry
entry = @schema.validate(ops, entry) # also does the update
entry['modifyTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
entry['modifiersName'] = [@connection.binddn.to_s]
@dir.data[dn] = entry
@dir.write
end
end
end
directory = Directory.new("ldapdb.yaml")
schema = LDAP::Server::Schema.new
schema.load_system
schema.load_file("../test/core.schema")
schema.resolve_oids
s = LDAP::Server.new(
:port => 1389,
:nodelay => true,
:listen => 10,
# :ssl_key_file => "key.pem",
# :ssl_cert_file => "cert.pem",
# :ssl_on_connect => true,
:operation_class => DirOperation,
:operation_args => [directory],
:schema => schema,
:namingContexts => ['dc=example,dc=com']
)
s.run_prefork
s.join
+90
View File
@@ -0,0 +1,90 @@
#!/usr/local/bin/ruby -w
# This is a modified version of rbslapd1.rb which uses a Router instead of
# subclassing the LDAP::Server::Operation class.
# This is a trivial LDAP server which just stores directory entries in RAM.
# It does no validation or authentication. This is intended just to
# demonstrate the API, it's not for real-world use!!
$:.unshift('../lib')
$debug = true
require 'ldap/server'
require 'ldap/server/router'
$logger = Logger.new($stderr)
class LDAPController
def self.bind(request, version, dn, password, params)
$logger.debug "Catchall bind request"
raise LDAP::ResultError::UnwillingToPerform, "Invalid bind DN"
end
def self.bindUser(request, version, dn, password, params)
if params[:uid].nil? or
params[:uid] != 'admin' or
password != 'adminpassword'
$logger.warn "Denied access for user #{params[:uid]}: Invalid credentials"
raise LDAP::ResultError::InvalidCredentials, "Invalid credentials"
end
$logger.info "Authenticated user #{params[:uid]}"
end
def self.search(request, baseObject, scope, deref, filter, params)
$logger.info "Catchall search request for #{baseObject}"
raise LDAP::ResultError::UnwillingToPerform, "Invalid search DN"
end
def self.searchUsers(request, baseObject, scope, deref, filter, params)
$logger.info "Search users"
end
end
router = LDAP::Server::Router.new($logger) do
# Different syntax but same thing
bind nil => "LDAPController#bind"
route :bind, nil => "LDAPController#bind"
# Bind a route using variables. A hash with the variables will be passed
# to your function as last argument.
bind "uid=:uid,ou=Users,dc=mydomain,dc=com" => "LDAPController#bindUser"
search nil => "LDAPController#search"
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#searchUsers"
end
# This is the shared object which carries our actual directory entries.
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
directory = {}
# Let's put some backing store on it
require 'yaml'
begin
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
rescue Errno::ENOENT
end
at_exit do
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
File.rename("ldapdb.new","ldapdb.yaml")
end
# Listen for incoming LDAP connections. For each one, create a Connection
# object, which will invoke a HashOperation object for each request.
s = LDAP::Server.new(
:port => 1389,
:nodelay => true,
:listen => 10,
# :ssl_key_file => "key.pem",
# :ssl_cert_file => "cert.pem",
# :ssl_on_connect => true,
:router => router
)
s.run_tcpserver
s.join
+73
View File
@@ -0,0 +1,73 @@
#!/usr/local/bin/ruby -w
# Example server that listens on both a port and a UNIX domain socket
# Try it using:
# $ ldapsearch -LLL -H ldap://localhost:1389 -D uid=whatever -b ou=Users,dc=mydomain,dc=com
# $ ldapsearch -LLL -H ldapi://%2ftmp%2frbslapd5.sock -D uid=whatever -b ou=Users,dc=mydomain,dc=com
$:.unshift('../lib')
$debug = true
require 'fileutils'
require 'ldap/server'
require 'ldap/server/router'
$logger = Logger.new($stderr)
class LDAPController
def self.bind(request, version, dn, password, params)
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'"
end
def self.search(request, baseObject, scope, deref, filter, params)
$logger.info "Processing search route for #{baseObject}"
h = {
'uid' => 'jdoe',
'objectClass' => 'userAccount',
'givenName' => 'John',
'sn' => 'Doe'
}
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h)
end
end
router = LDAP::Server::Router.new($logger) do
bind nil => "LDAPController#bind"
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search"
end
params = {
:nodelay => true,
:listen => 10,
:router => router
}
# Listen on IP address and port
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0
params[:port] = 1389
addr_server = LDAP::Server.new params
addr_server.run_tcpserver
# Listen on socket
params.delete :bindaddr
params.delete :port
params[:socket] = '/tmp/rbslapd5.sock'
FileUtils::rm_f params[:socket]
socket_server = LDAP::Server.new params
socket_server.run_tcpserver
trap 'INT' do
addr_server.stop
socket_server.stop
end
addr_server.join
socket_server.join
+75
View File
@@ -0,0 +1,75 @@
#!/usr/local/bin/ruby -w
# Slightly modified version of rbslapd5.rb which demonstrates dropping
# root privileges after binding to port 389
#
# Run this script with `sudo`
$:.unshift('../lib')
$debug = true
require 'fileutils'
require 'ldap/server'
require 'ldap/server/router'
$logger = Logger.new($stderr)
class LDAPController
def self.bind(request, version, dn, password, params)
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'"
end
def self.search(request, baseObject, scope, deref, filter, params)
$logger.info "Processing search route for #{baseObject}"
h = {
'uid' => 'jdoe',
'objectClass' => 'userAccount',
'givenName' => 'John',
'sn' => 'Doe'
}
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h)
end
end
router = LDAP::Server::Router.new($logger) do
bind nil => "LDAPController#bind"
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search"
end
params = {
:nodelay => true,
:listen => 10,
:router => router
}
# Listen on IP address and port
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0
params[:port] = 389
params[:user] = 'ldap'
params[:group] = 'ldap'
addr_server = LDAP::Server.new params
addr_server.run_tcpserver
# Listen on socket
params.delete :bindaddr
params.delete :port
params[:socket] = '/tmp/rbslapd6.sock'
FileUtils::rm_f params[:socket]
socket_server = LDAP::Server.new params
socket_server.run_tcpserver
trap 'INT' do
addr_server.stop
socket_server.stop
end
addr_server.join
socket_server.join
+37
View File
@@ -0,0 +1,37 @@
#!/usr/local/bin/ruby
require 'ldap'
CHILDREN = 10
CONNECTS = 1 # per child
SEARCHES = 100 # per connection
pids = []
CHILDREN.times do
pids << fork do
CONNECTS.times do
conn = LDAP::Conn.new("localhost",1389)
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
conn.bind
SEARCHES.times do
res = conn.search("cn=Fred Flintstone,dc=example,dc=com", LDAP::LDAP_SCOPE_BASE,
"(objectclass=*)") do |e|
#puts "#{$$} #{e.dn.inspect}"
end
end
conn.unbind
end
end
end
okcount = 0
badcount = 0
pids.each do |p|
Process.wait(p)
if $?.exitstatus == 0
okcount += 1
else
badcount += 1
end
end
puts "Children finished: #{okcount} ok, #{badcount} failed"
exit badcount > 0 ? 1 : 0