init
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
require 'thread'
|
||||
require 'openssl'
|
||||
require 'ldap/server/result'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# An object which handles an LDAP connection. Note that LDAP allows
|
||||
# requests and responses to be exchanged asynchronously: e.g. a client
|
||||
# can send three requests, and the three responses can come back in
|
||||
# any order. For that reason, we start a new thread for each request,
|
||||
# and we need a mutex on the io object so that multiple responses don't
|
||||
# interfere with each other.
|
||||
|
||||
class Connection
|
||||
attr_reader :binddn, :version, :opt
|
||||
|
||||
def initialize(io, opt={})
|
||||
@io = io
|
||||
@opt = opt
|
||||
@mutex = Mutex.new
|
||||
@threadgroup = ThreadGroup.new
|
||||
@binddn = nil
|
||||
@version = 3
|
||||
@logger = @opt[:logger]
|
||||
@ssl = false
|
||||
|
||||
startssl if @opt[:ssl_on_connect]
|
||||
end
|
||||
|
||||
def log(msg, severity = Logger::INFO)
|
||||
@logger.add(severity, msg, @io.peeraddr[3])
|
||||
end
|
||||
|
||||
def debug msg
|
||||
log msg, Logger::DEBUG
|
||||
end
|
||||
|
||||
def log_exception(e)
|
||||
log "#{e}: #{e.backtrace.join("\n\tfrom ")}", Logger::ERROR
|
||||
end
|
||||
|
||||
def startssl # :yields:
|
||||
@mutex.synchronize do
|
||||
raise LDAP::ResultError::OperationsError if @ssl or @threadgroup.list.size > 0
|
||||
yield if block_given?
|
||||
@io = OpenSSL::SSL::SSLSocket.new(@io, @opt[:ssl_ctx])
|
||||
@io.sync_close = true
|
||||
@io.accept
|
||||
@ssl = true
|
||||
end
|
||||
end
|
||||
|
||||
# Read one ASN1 element from the given stream.
|
||||
# Return String containing the raw element.
|
||||
|
||||
def ber_read(io)
|
||||
blk = io.read(2) # minimum: short tag, short length
|
||||
throw(:close) if blk.nil?
|
||||
|
||||
codepoints = blk.respond_to?(:codepoints) ? blk.codepoints.to_a : blk
|
||||
|
||||
tag = codepoints[0] & 0x1f
|
||||
len = codepoints[1]
|
||||
|
||||
if tag == 0x1f # long form
|
||||
tag = 0
|
||||
while true
|
||||
ch = io.getc
|
||||
blk << ch
|
||||
tag = (tag << 7) | (ch & 0x7f)
|
||||
break if (ch & 0x80) == 0
|
||||
end
|
||||
len = io.getc
|
||||
blk << len
|
||||
end
|
||||
|
||||
if (len & 0x80) != 0 # long form
|
||||
len = len & 0x7f
|
||||
raise LDAP::ResultError::ProtocolError, "Indefinite length encoding not supported" if len == 0
|
||||
offset = blk.length
|
||||
blk << io.read(len)
|
||||
# is there a more efficient way of doing this?
|
||||
len = 0
|
||||
blk[offset..-1].each_byte { |b| len = (len << 8) | b }
|
||||
end
|
||||
|
||||
offset = blk.length
|
||||
blk << io.read(len)
|
||||
return blk
|
||||
# or if we wanted to keep the partial decoding we've done:
|
||||
# return blk, [blk[0] >> 6, tag], offset
|
||||
end
|
||||
|
||||
def handle_requests
|
||||
catch(:close) do
|
||||
while true
|
||||
begin
|
||||
blk = ber_read(@io)
|
||||
asn1 = OpenSSL::ASN1::decode(blk)
|
||||
# Debugging:
|
||||
# puts "Request: #{blk.unpack("H*")}\n#{asn1.inspect}" if $debug
|
||||
|
||||
raise LDAP::ResultError::ProtocolError, "LDAPMessage must be SEQUENCE" unless asn1.is_a?(OpenSSL::ASN1::Sequence)
|
||||
raise LDAP::ResultError::ProtocolError, "Bad Message ID" unless asn1.value[0].is_a?(OpenSSL::ASN1::Integer)
|
||||
messageId = asn1.value[0].value
|
||||
|
||||
protocolOp = asn1.value[1]
|
||||
raise LDAP::ResultError::ProtocolError, "Bad protocolOp" unless protocolOp.is_a?(OpenSSL::ASN1::ASN1Data)
|
||||
raise LDAP::ResultError::ProtocolError, "Bad protocolOp tag class" unless protocolOp.tag_class == :APPLICATION
|
||||
|
||||
# controls are not properly implemented
|
||||
c = asn1.value[2]
|
||||
if c.is_a?(OpenSSL::ASN1::ASN1Data) and c.tag_class == :APPLICATION and c.tag == 0
|
||||
controls = c.value
|
||||
end
|
||||
|
||||
case protocolOp.tag
|
||||
when 0 # BindRequest
|
||||
abandon_all
|
||||
if @opt[:router]
|
||||
@binddn, @version = @opt[:router].do_bind(self, messageId, protocolOp, controls)
|
||||
else
|
||||
operationClass = @opt[:operation_class]
|
||||
ocArgs = @opt[:operation_args] || []
|
||||
@binddn, @version = operationClass.new(self,messageId,*ocArgs).
|
||||
do_bind(protocolOp, controls)
|
||||
end
|
||||
when 2 # UnbindRequest
|
||||
throw(:close)
|
||||
|
||||
when 3 # SearchRequest
|
||||
start_op(messageId,protocolOp,controls,:do_search)
|
||||
|
||||
when 6 # ModifyRequest
|
||||
start_op(messageId,protocolOp,controls,:do_modify)
|
||||
|
||||
when 8 # AddRequest
|
||||
start_op(messageId,protocolOp,controls,:do_add)
|
||||
|
||||
when 10 # DelRequest
|
||||
start_op(messageId,protocolOp,controls,:do_del)
|
||||
|
||||
when 12 # ModifyDNRequest
|
||||
start_op(messageId,protocolOp,controls,:do_modifydn)
|
||||
|
||||
when 14 # CompareRequest
|
||||
start_op(messageId,protocolOp,controls,:do_compare)
|
||||
|
||||
when 16 # AbandonRequest
|
||||
abandon(protocolOp.value)
|
||||
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError, "Unrecognised protocolOp tag #{protocolOp.tag}"
|
||||
end
|
||||
|
||||
rescue LDAP::ResultError::ProtocolError, OpenSSL::ASN1::ASN1Error => e
|
||||
send_notice_of_disconnection(LDAP::ResultError::ProtocolError.new.to_i, e.message)
|
||||
throw(:close)
|
||||
|
||||
# all other exceptions propagate up and are caught by tcpserver
|
||||
end
|
||||
end
|
||||
end
|
||||
abandon_all
|
||||
end
|
||||
|
||||
# Start an operation in a Thread. Add this to a ThreadGroup to allow
|
||||
# the operation to be abandoned later.
|
||||
#
|
||||
# When the thread terminates, it automatically drops out of the group.
|
||||
#
|
||||
# Note: RFC 2251 4.4.4.1 says behaviour is undefined if
|
||||
# client sends an overlapping request with same message ID,
|
||||
# so we don't have to worry about the case where there is
|
||||
# already a thread with this messageId in @threadgroup.
|
||||
def start_op(messageId,protocolOp,controls,meth)
|
||||
operationClass = @opt[:operation_class]
|
||||
ocArgs = @opt[:operation_args] || []
|
||||
thr = Thread.new do
|
||||
begin
|
||||
if @opt[:router]
|
||||
@opt[:router].send meth, self, messageId, protocolOp, controls
|
||||
else
|
||||
operationClass.new(self,messageId,*ocArgs).
|
||||
send(meth,protocolOp,controls)
|
||||
end
|
||||
rescue Exception => e
|
||||
log_exception e
|
||||
end
|
||||
end
|
||||
thr[:messageId] = messageId
|
||||
@threadgroup.add(thr)
|
||||
end
|
||||
|
||||
def write(data)
|
||||
@mutex.synchronize do
|
||||
@io.write(data)
|
||||
@io.flush
|
||||
end
|
||||
end
|
||||
|
||||
def writelock
|
||||
@mutex.synchronize do
|
||||
yield @io
|
||||
@io.flush
|
||||
end
|
||||
end
|
||||
|
||||
def abandon(messageID)
|
||||
@mutex.synchronize do
|
||||
thread = @threadgroup.list.find { |t| t[:messageId] == messageID }
|
||||
thread.raise LDAP::Abandon if thread
|
||||
end
|
||||
end
|
||||
|
||||
def abandon_all
|
||||
@mutex.synchronize do
|
||||
@threadgroup.list.each do |thread|
|
||||
thread.raise LDAP::Abandon
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def send_unsolicited_notification(resultCode, opt={})
|
||||
protocolOp = [
|
||||
OpenSSL::ASN1::Enumerated(resultCode),
|
||||
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
|
||||
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
|
||||
]
|
||||
if opt[:referral]
|
||||
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
|
||||
protocolOp << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
if opt[:responseName]
|
||||
protocolOp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
if opt[:response]
|
||||
protocolOp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
message = [
|
||||
OpenSSL::ASN1::Integer(0),
|
||||
OpenSSL::ASN1::Sequence(protocolOp, 24, :IMPLICIT, :APPLICATION),
|
||||
]
|
||||
message << opt[:controls] if opt[:controls]
|
||||
write(OpenSSL::ASN1::Sequence(message).to_der)
|
||||
end
|
||||
|
||||
def send_notice_of_disconnection(resultCode, errorMessage="")
|
||||
send_unsolicited_notification(resultCode,
|
||||
:errorMessage=>errorMessage,
|
||||
:responseName=>"1.3.6.1.4.1.1466.20036"
|
||||
)
|
||||
end
|
||||
end
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,220 @@
|
||||
require 'ldap/server/util'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
class DN
|
||||
include Enumerable
|
||||
|
||||
attr_reader :dname
|
||||
|
||||
# Combines a set of elements to a syntactically correct DN
|
||||
# elements is [elements, ...] where elements
|
||||
# can be either { attr => val } or [attr, val]
|
||||
def self.join(elements)
|
||||
LDAP::Server::Operation.join_dn(elements)
|
||||
end
|
||||
|
||||
def initialize(dn)
|
||||
@dname = LDAP::Server::Operation.split_dn(dn)
|
||||
end
|
||||
|
||||
# Returns the value of the first occurrence of attr (bottom-up)
|
||||
def find_first(attr)
|
||||
@dname.each do |pair|
|
||||
return pair[attr.to_s] if pair[attr.to_s]
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Returns the value of the last occurrence of attr (bottom-up)
|
||||
def find_last(attr)
|
||||
@dname.reverse_each do |pair|
|
||||
return pair[attr.to_s] if pair[attr.to_s]
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Returns all values of all occurrences of attr (bottom-up)
|
||||
def find(attr)
|
||||
result = []
|
||||
@dname.each do |pair|
|
||||
result << pair[attr.to_s] if pair[attr.to_s]
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# Returns the value of the n-th occurrence of attr (top-down, 0 is first element)
|
||||
def find_nth(attr, n)
|
||||
i = 0
|
||||
@dname.each do |pair|
|
||||
if pair[attr.to_s]
|
||||
return pair[attr.to_s] if i == n
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Whether or not the DN starts with dn (bottom-up)
|
||||
# dn is a string
|
||||
def start_with?(dn)
|
||||
needle = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
# Needle is longer than haystack
|
||||
return false if needle.length > @dname.length
|
||||
|
||||
needle_index = 0
|
||||
haystack_index = 0
|
||||
|
||||
while needle_index < needle.length
|
||||
return false if @dname[haystack_index] != needle[needle_index]
|
||||
needle_index += 1
|
||||
haystack_index += 1
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN starts with a format (bottom-up) (values are ignored)
|
||||
# dn is a string
|
||||
def start_with_format?(dn)
|
||||
needle = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
# Needle is longer than haystack
|
||||
return false if needle.length > @dname.length
|
||||
|
||||
needle_index = 0
|
||||
haystack_index = 0
|
||||
|
||||
while needle_index < needle.length
|
||||
return false if @dname[haystack_index].keys != needle[needle_index].keys
|
||||
needle_index += 1
|
||||
haystack_index += 1
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN ends with dn (top-down)
|
||||
# dn is a string
|
||||
def end_with?(dn)
|
||||
needle = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
# Needle is longer than haystack
|
||||
return false if needle.length > @dname.length
|
||||
|
||||
needle_index = needle.length - 1
|
||||
haystack_index = @dname.length - 1
|
||||
|
||||
while needle_index >= 0
|
||||
return false if @dname[haystack_index] != needle[needle_index]
|
||||
needle_index -= 1
|
||||
haystack_index -= 1
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN ends with format (top-down) (values are ignored)
|
||||
# dn is a string
|
||||
def end_with_format?(dn)
|
||||
needle = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
# Needle is longer than haystack
|
||||
return false if needle.length > @dname.length
|
||||
|
||||
needle_index = needle.length - 1
|
||||
haystack_index = @dname.length - 1
|
||||
|
||||
while needle_index >= 0
|
||||
return false if @dname[haystack_index].keys != needle[needle_index].keys
|
||||
needle_index -= 1
|
||||
haystack_index -= 1
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN equals dn (values are case sensitive)
|
||||
# dn is a string
|
||||
def equal?(dn)
|
||||
split_dn = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
return false if split_dn.length != @dname.length
|
||||
|
||||
@dname.each_with_index do |pair, index|
|
||||
return false if pair != split_dn[index]
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN equals dn's format (values are ignored) (case insensitive)
|
||||
# dn is a string
|
||||
def equal_format?(dn)
|
||||
split_dn = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
return false if split_dn.length != @dname.length
|
||||
|
||||
@dname.each_with_index do |pair, index|
|
||||
return false if pair.keys != split_dn[index].keys
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Whether or not the DN constains a substring equal to dn (values are case sensitive)
|
||||
# dn is a string
|
||||
def include?(dn)
|
||||
split_dn = LDAP::Server::Operation.split_dn(dn)
|
||||
return false if split_dn.length > @dname.length
|
||||
LDAP::Server::Operation.join_dn(@dname).include?(LDAP::Server::Operation.join_dn(split_dn))
|
||||
end
|
||||
|
||||
# Whether or not the DN constains a substring format equal to dn (values are ignored) (case insensitive)
|
||||
# dn is a string
|
||||
def include_format?(dn)
|
||||
split_dn = LDAP::Server::Operation.split_dn(dn)
|
||||
|
||||
return false if split_dn.length > @dname.length
|
||||
|
||||
haystack = []
|
||||
@dname.each { |pair| haystack << pair.keys }
|
||||
|
||||
needle = []
|
||||
split_dn.each { |pair| needle << pair.keys }
|
||||
|
||||
haystack.join.include?(needle.join)
|
||||
end
|
||||
|
||||
# Generates a mapping for variables
|
||||
# For example:
|
||||
# > dn = LDAP::Server.DN.new("uid=user,ou=Users,dc=mydomain,dc=com")
|
||||
# > dn.parse("uid=:uid, ou=:category, dc=mydomain, dc=com")
|
||||
# => { :uid => "user", :category => "Users" }
|
||||
def parse(template_dn)
|
||||
result = {}
|
||||
template = LDAP::Server::Operation.split_dn(template_dn)
|
||||
template.reverse.zip(@dname.reverse).each do |temp, const|
|
||||
break if const and temp.keys.first != const.keys.first
|
||||
if temp.values.first.start_with?(':')
|
||||
sym = temp.values.first[1..-1].to_sym
|
||||
if const
|
||||
result[sym] = const.values.first unless result[sym]
|
||||
else
|
||||
result[sym] = nil
|
||||
end
|
||||
elsif temp.values.first != const.values.first
|
||||
break
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@dname.each(&block)
|
||||
end
|
||||
|
||||
def reverse_each(&block)
|
||||
@dname.reverse_each(&block)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,223 @@
|
||||
require 'ldap/server/result'
|
||||
require 'ldap/server/match'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# LDAP filters are parsed into a LISP-like internal representation:
|
||||
#
|
||||
# [:true]
|
||||
# [:false]
|
||||
# [:undef]
|
||||
# [:and, ..., ..., ...]
|
||||
# [:or, ..., ..., ...]
|
||||
# [:not, ...]
|
||||
# [:present, attr]
|
||||
# [:eq, attr, MO, val]
|
||||
# [:approx, attr, MO, val]
|
||||
# [:substrings, attr, MO, initial=nil, {any, any...}, final=nil]
|
||||
# [:ge, attr, MO, val]
|
||||
# [:le, attr, MO, val]
|
||||
#
|
||||
# This is done rather than a more object-oriented approach, in the
|
||||
# hope that it will make it easier to match certain filter structures
|
||||
# when converting them into something else. e.g. certain LDAP filter
|
||||
# constructs can be mapped to some fixed SQL queries.
|
||||
#
|
||||
# See RFC 2251 4.5.1 for the three-state(!) boolean logic from LDAP
|
||||
#
|
||||
# If no schema is provided: 'attr' is the raw attribute name as provided
|
||||
# by the client. If a schema is provided: attr is converted to its
|
||||
# normalized name as listed in the schema, e.g. 'commonname' becomes 'cn',
|
||||
# 'objectclass' becomes 'objectClass' etc.
|
||||
# If a schema is provided, MO is a matching object which can be used to
|
||||
# perform the match. If no schema is provided, this is 'nil'. In that
|
||||
# case you could use LDAP::Server::MatchingRule::DefaultMatch.
|
||||
|
||||
class Filter
|
||||
|
||||
# Parse a filter in OpenSSL::ASN1 format into our own format.
|
||||
#
|
||||
# There are some trivial optimisations we make: e.g.
|
||||
# (&(objectClass=*)(cn=foo)) -> (&(cn=foo)) -> (cn=foo)
|
||||
|
||||
def self.parse(asn1, schema=nil)
|
||||
case asn1.tag
|
||||
when 0 # and
|
||||
conds = asn1.value.collect { |a| parse(a) }
|
||||
conds.delete([:true])
|
||||
return [:true] if conds.size == 0
|
||||
return conds.first if conds.size == 1
|
||||
return [:false] if conds.include?([:false])
|
||||
return conds.unshift(:and)
|
||||
|
||||
when 1 # or
|
||||
conds = asn1.value.collect { |a| parse(a) }
|
||||
conds.delete([:false])
|
||||
return [:false] if conds.size == 0
|
||||
return conds.first if conds.size == 1
|
||||
return [:true] if conds.include?([:true])
|
||||
return conds.unshift(:or)
|
||||
|
||||
when 2 # not
|
||||
cond = parse(asn1.value[0])
|
||||
case cond
|
||||
when [:false]; return [:true]
|
||||
when [:true]; return [:false]
|
||||
when [:undef]; return [:undef]
|
||||
end
|
||||
return [:not, cond]
|
||||
|
||||
when 3 # equalityMatch
|
||||
attr = asn1.value[0].value
|
||||
val = asn1.value[1].value
|
||||
return [:true] if attr =~ /\AobjectClass\z/i and val =~ /\Atop\z/i
|
||||
if schema
|
||||
a = schema.find_attrtype(attr)
|
||||
return [:undef] unless a.equality
|
||||
return [:eq, a.to_s, a.equality, val]
|
||||
end
|
||||
return [:eq, attr, nil, val]
|
||||
|
||||
when 4 # substrings
|
||||
attr = asn1.value[0].value
|
||||
if schema
|
||||
a = schema.find_attrtype(attr)
|
||||
return [:undef] unless a.substr
|
||||
res = [:substrings, a.to_s, a.substr, nil]
|
||||
else
|
||||
res = [:substrings, attr, nil, nil]
|
||||
end
|
||||
final_val = nil
|
||||
|
||||
asn1.value[1].value.each do |ss|
|
||||
case ss.tag
|
||||
when 0
|
||||
res[3] = ss.value
|
||||
when 1
|
||||
res << ss.value
|
||||
when 2
|
||||
final_val = ss.value
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError,
|
||||
"Unrecognised substring tag #{ss.tag.inspect}"
|
||||
end
|
||||
end
|
||||
res << final_val
|
||||
return res
|
||||
|
||||
when 5 # greaterOrEqual
|
||||
attr = asn1.value[0].value
|
||||
val = asn1.value[1].value
|
||||
if schema
|
||||
a = schema.find_attrtype(attr)
|
||||
return [:undef] unless a.ordering
|
||||
return [:ge, a.to_s, a.ordering, val]
|
||||
end
|
||||
return [:ge, attr, nil, val]
|
||||
|
||||
when 6 # lessOrEqual
|
||||
attr = asn1.value[0].value
|
||||
val = asn1.value[1].value
|
||||
if schema
|
||||
a = schema.find_attrtype(attr)
|
||||
return [:undef] unless a.ordering
|
||||
return [:le, a.to_s, a.ordering, val]
|
||||
end
|
||||
return [:le, attr, nil, val]
|
||||
|
||||
when 7 # present
|
||||
attr = asn1.value
|
||||
return [:true] if attr =~ /\AobjectClass\z/i
|
||||
if schema
|
||||
begin
|
||||
a = schema.find_attrtype(attr)
|
||||
return [:present, a.to_s]
|
||||
rescue LDAP::ResultError::UndefinedAttributeType
|
||||
return [:false]
|
||||
end
|
||||
end
|
||||
return [:present, attr]
|
||||
|
||||
when 8 # approxMatch
|
||||
attr = asn1.value[0].value
|
||||
val = asn1.value[1].value
|
||||
if schema
|
||||
a = schema.find_attrtype(attr)
|
||||
# I don't know how properly to deal with approxMatch. I'm assuming
|
||||
# that the object will have an equality MatchingRule, and we
|
||||
# can defer to that.
|
||||
return [:undef] unless a.equality
|
||||
return [:approx, a.to_s, a.equality, val]
|
||||
end
|
||||
return [:approx, attr, nil, val]
|
||||
|
||||
#when 9 # extensibleMatch
|
||||
# FIXME
|
||||
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError,
|
||||
"Unrecognised Filter tag #{asn1.tag}"
|
||||
end
|
||||
|
||||
# Unknown attribute type
|
||||
rescue LDAP::ResultError::UndefinedAttributeType
|
||||
return [:undef]
|
||||
end
|
||||
|
||||
# Run a parsed filter against an attr=>[val] hash.
|
||||
#
|
||||
# Returns true, false or nil.
|
||||
|
||||
def self.run(filter, av)
|
||||
case filter[0]
|
||||
when :and
|
||||
res = true
|
||||
filter[1..-1].each do |elem|
|
||||
r = run(elem, av)
|
||||
return false if r == false
|
||||
res = nil if r.nil?
|
||||
end
|
||||
return res
|
||||
|
||||
when :or
|
||||
res = false
|
||||
filter[1..-1].each do |elem|
|
||||
r = run(elem, av)
|
||||
return true if r == true
|
||||
res = nil if r.nil?
|
||||
end
|
||||
return res
|
||||
|
||||
when :not
|
||||
case run(filter[1], av)
|
||||
when true; return false
|
||||
when false; return true
|
||||
else return nil
|
||||
end
|
||||
|
||||
when :present
|
||||
return av.has_key?(filter[1])
|
||||
|
||||
when :eq, :approx, :le, :ge, :substrings
|
||||
# the filter now includes a suitable matching object
|
||||
return (filter[2] || LDAP::Server::MatchingRule::DefaultMatch).send(
|
||||
filter.first, Array(av[filter[1].to_s]), *filter[3..-1])
|
||||
|
||||
when :true
|
||||
return true
|
||||
|
||||
when :false
|
||||
return false
|
||||
|
||||
when :undef
|
||||
return nil
|
||||
end
|
||||
|
||||
raise LDAP::ResultError::OperationsError,
|
||||
"Unimplemented filter #{filter.first.inspect}"
|
||||
end
|
||||
|
||||
end # class Filter
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,283 @@
|
||||
require 'ldap/server/syntax'
|
||||
require 'ldap/server/result'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# A class which holds LDAP MatchingRules. For now there is a global pool
|
||||
# of MatchingRule objects (rather than each Schema object having
|
||||
# its own pool)
|
||||
|
||||
class MatchingRule
|
||||
attr_reader :oid, :names, :syntax, :desc, :obsolete
|
||||
|
||||
# Create a new MatchingRule object
|
||||
|
||||
def initialize(oid, names, syntax, desc=nil, obsolete=false, &blk)
|
||||
@oid = oid
|
||||
@names = names
|
||||
@names = [@names] unless @names.is_a?(Array)
|
||||
@desc = desc
|
||||
@obsolete = obsolete
|
||||
@syntax = LDAP::Server::Syntax.find(syntax) # creates new obj if reqd
|
||||
@def = nil
|
||||
# initialization hook
|
||||
self.instance_eval(&blk) if blk
|
||||
end
|
||||
|
||||
def name
|
||||
(@names && names[0]) || @oid
|
||||
end
|
||||
|
||||
def to_s
|
||||
(@names && names[0]) || @oid
|
||||
end
|
||||
|
||||
def normalize(x)
|
||||
x
|
||||
end
|
||||
|
||||
# Create a new MatchingRule object, given its description string
|
||||
|
||||
def self.from_def(str, &blk)
|
||||
m = LDAP::Server::Syntax::MatchingRuleDescription.match(str)
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Bad MatchingRuleDescription #{str.inspect}" unless m
|
||||
new(m[1], m[2].scan(/'(.*?)'/).flatten, m[5], m[3], m[4], &blk)
|
||||
end
|
||||
|
||||
def to_def
|
||||
return @def if @def
|
||||
ans = "( #{@oid} "
|
||||
if names.nil? or @names.empty?
|
||||
# nothing
|
||||
elsif @names.size == 1
|
||||
ans << "NAME '#{@names[0]}' "
|
||||
else
|
||||
ans << "NAME ( "
|
||||
@names.each { |n| ans << "'#{n}' " }
|
||||
ans << ") "
|
||||
end
|
||||
ans << "DESC '#@desc' " if @desc
|
||||
ans << "OBSOLETE " if @obsolete
|
||||
ans << "SYNTAX #@syntax " if @syntax
|
||||
ans << ")"
|
||||
@def = ans
|
||||
end
|
||||
|
||||
@@rules = {} # oid / name / alias => object
|
||||
|
||||
# Add a new matching rule
|
||||
|
||||
def self.add(*args, &blk)
|
||||
s = new(*args, &blk)
|
||||
@@rules[s.oid] = s
|
||||
return if s.names.nil?
|
||||
s.names.each do |n|
|
||||
@@rules[n.downcase] = s
|
||||
end
|
||||
end
|
||||
|
||||
# Find a MatchingRule object given a name or oid, or return nil
|
||||
# (? should we create one automatically, like Syntax)
|
||||
|
||||
def self.find(x)
|
||||
return x if x.nil? or x.is_a?(LDAP::Server::MatchingRule)
|
||||
@@rules[x.downcase]
|
||||
end
|
||||
|
||||
# Return all known matching rules
|
||||
|
||||
def self.all_matching_rules
|
||||
@@rules.values.uniq
|
||||
end
|
||||
|
||||
# Now some things we can mixin to a MatchingRule when needed.
|
||||
# Replace 'normalize' with a function which gives the canonical
|
||||
# version of a value for comparison.
|
||||
|
||||
module Equality
|
||||
def eq(vals, m)
|
||||
return false if vals.nil?
|
||||
m = normalize(m)
|
||||
vals.each { |v| return true if normalize(v) == m }
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
module Ordering
|
||||
def ge(vals, m)
|
||||
return false if vals.nil?
|
||||
m = normalize(m)
|
||||
vals.each { |v| return true if normalize(v) >= m }
|
||||
return false
|
||||
end
|
||||
|
||||
def le(vals, m)
|
||||
return false if vals.nil?
|
||||
m = normalize(m)
|
||||
vals.each { |v| return true if normalize(v) <= m }
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
module Substrings
|
||||
def substrings(vals, *ss)
|
||||
return false if vals.nil?
|
||||
|
||||
# convert the condition list into a regexp
|
||||
re = []
|
||||
re << "^#{Regexp.escape(normalize(ss[0]).to_s)}" if ss[0]
|
||||
ss[1..-2].each { |s| re << Regexp.escape(normalize(s).to_s) }
|
||||
re << "#{Regexp.escape(normalize(ss[-1]).to_s)}$" if ss[-1]
|
||||
re = Regexp.new(re.join(".*"))
|
||||
|
||||
vals.each do |v|
|
||||
v = normalize(v).to_s
|
||||
return true if re.match(v)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end # module Substrings
|
||||
|
||||
class DefaultMatchingClass
|
||||
include MatchingRule::Equality
|
||||
include MatchingRule::Ordering
|
||||
include MatchingRule::Substrings
|
||||
def normalize(x)
|
||||
x
|
||||
end
|
||||
end
|
||||
|
||||
DefaultMatch = DefaultMatchingClass.new
|
||||
|
||||
end # class MatchingRule
|
||||
|
||||
#
|
||||
# And now, here are some matching rules you can use (RFC2252 section 8)
|
||||
#
|
||||
|
||||
class MatchingRule
|
||||
|
||||
add('2.5.13.0', 'objectIdentifierMatch', '1.3.6.1.4.1.1466.115.121.1.38') do
|
||||
extend Equality
|
||||
end
|
||||
# FIXME: Filters should return undef if the OID is not in the schema
|
||||
# (which means passing in the schema to every equality test)
|
||||
|
||||
add('2.5.13.1', 'distinguishedNameMatch', '1.3.6.1.4.1.1466.115.121.1.12') do
|
||||
extend Equality
|
||||
end
|
||||
# FIXME: Distinguished Name matching is supposed to parse the DN into
|
||||
# its parts and then apply the schema equality rules to each part
|
||||
# (i.e. some parts may be case-sensitive, others case-insensitive)
|
||||
# This is just one of the many nonsense design decisions in LDAP :-(
|
||||
|
||||
# How is a DirectoryString different to an IA5String or a PrintableString?
|
||||
|
||||
module StringTrim
|
||||
def normalize(x); x.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
|
||||
end
|
||||
|
||||
module StringDowncase
|
||||
def normalize(x); x.downcase.gsub(/^\s*|\s*$/, '').gsub(/\s+/,' '); end
|
||||
end
|
||||
|
||||
add('2.5.13.2', 'caseIgnoreMatch', '1.3.6.1.4.1.1466.115.1') do
|
||||
extend Equality
|
||||
extend StringDowncase
|
||||
end
|
||||
|
||||
module Integer
|
||||
def normalize(x); x.to_i; end
|
||||
end
|
||||
|
||||
add('2.5.13.8', 'numericStringMatch', '1.3.6.1.4.1.1466.115.121.1.36') do
|
||||
extend Equality
|
||||
extend Integer
|
||||
end
|
||||
|
||||
# TODO: Add semantics for these (difficult since RFC2252 doesn't give
|
||||
# them, so we presumably have to go through X.500)
|
||||
add('2.5.13.11', 'caseIgnoreListMatch', '1.3.6.1.4.1.1466.115.121.1.41')
|
||||
add('2.5.13.14', 'integerMatch', '1.3.6.1.4.1.1466.115.121.1.27') do
|
||||
extend Equality
|
||||
extend Integer
|
||||
end
|
||||
add('2.5.13.16', 'bitStringMatch', '1.3.6.1.4.1.1466.115.121.1.6')
|
||||
add('2.5.13.20', 'telephoneNumberMatch', '1.3.6.1.4.1.1466.115.121.1.50') do
|
||||
extend Equality
|
||||
extend StringTrim
|
||||
end
|
||||
add('2.5.13.22', 'presentationAddressMatch', '1.3.6.1.4.1.1466.115.121.1.43')
|
||||
add('2.5.13.23', 'uniqueMemberMatch', '1.3.6.1.4.1.1466.115.121.1.34')
|
||||
add('2.5.13.24', 'protocolInformationMatch', '1.3.6.1.4.1.1466.115.121.1.42')
|
||||
add('2.5.13.27', 'generalizedTimeMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Equality }
|
||||
|
||||
# IA5 stuff. FIXME: What's the correct way to 'downcase' UTF8 strings?
|
||||
|
||||
module IA5Trim
|
||||
def normalize(x); x.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
|
||||
end
|
||||
|
||||
module IA5Downcase
|
||||
def normalize(x); x.downcase.gsub(/^\s*|\s*$/u, '').gsub(/\s+/u,' '); end
|
||||
end
|
||||
|
||||
add('1.3.6.1.4.1.1466.109.114.1', 'caseExactIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
|
||||
extend Equality
|
||||
extend IA5Trim
|
||||
end
|
||||
|
||||
add('1.3.6.1.4.1.1466.109.114.2', 'caseIgnoreIA5Match', '1.3.6.1.4.1.1466.115.121.1.26') do
|
||||
extend Equality
|
||||
extend IA5Downcase
|
||||
end
|
||||
|
||||
add('2.5.13.28', 'generalizedTimeOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.24') { extend Ordering }
|
||||
add('2.5.13.3', 'caseIgnoreOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') do
|
||||
extend Ordering
|
||||
extend StringDowncase
|
||||
end
|
||||
|
||||
add('2.5.13.4', 'caseIgnoreSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
||||
extend Substrings
|
||||
extend StringDowncase
|
||||
end
|
||||
add('2.5.13.21', 'telephoneNumberSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
||||
extend Substrings
|
||||
end
|
||||
add('2.5.13.10', 'numericStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') do
|
||||
extend Substrings
|
||||
end
|
||||
|
||||
# from OpenLDAP
|
||||
add('1.3.6.1.4.1.4203.1.2.1', 'caseExactIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
|
||||
extend Substrings
|
||||
extend IA5Trim
|
||||
end
|
||||
add('1.3.6.1.4.1.1466.109.114.3', 'caseIgnoreIA5SubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.26') do
|
||||
extend Substrings
|
||||
extend IA5Downcase
|
||||
end
|
||||
add('2.5.13.5', 'caseExactMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Equality }
|
||||
add('2.5.13.6', 'caseExactOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.15') { extend Ordering }
|
||||
add('2.5.13.7', 'caseExactSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.58') { extend Substrings }
|
||||
add('2.5.13.9', 'numericStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.36') { extend Ordering; extend Integer }
|
||||
add('2.5.13.13', 'booleanMatch', '1.3.6.1.4.1.1466.115.121.1.7') do
|
||||
extend Equality
|
||||
def self.normalize(x)
|
||||
return true if x == 'TRUE'
|
||||
return false if x == 'FALSE'
|
||||
x
|
||||
end
|
||||
end
|
||||
add('2.5.13.15', 'integerOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.27') { extend Ordering; extend Integer }
|
||||
add('2.5.13.17', 'octetStringMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Equality }
|
||||
add('2.5.13.18', 'octetStringOrderingMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Ordering }
|
||||
add('2.5.13.19', 'octetStringSubstringsMatch', '1.3.6.1.4.1.1466.115.121.1.40') { extend Substrings }
|
||||
|
||||
end # class MatchingRule
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,528 @@
|
||||
require 'timeout'
|
||||
require 'openssl'
|
||||
require 'ldap/server/result'
|
||||
require 'ldap/server/filter'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# Scope
|
||||
BaseObject = 0
|
||||
SingleLevel = 1
|
||||
WholeSubtree = 2
|
||||
|
||||
# DerefAliases
|
||||
NeverDerefAliases = 0
|
||||
DerefInSearching = 1
|
||||
DerefFindingBaseObj = 2
|
||||
DerefAlways = 3
|
||||
|
||||
# Object to handle a single LDAP request. Typically you would
|
||||
# subclass this object and override methods 'simple_bind', 'search' etc.
|
||||
# The do_xxx methods are internal, and handle the parsing of requests
|
||||
# and the sending of responses.
|
||||
|
||||
class Operation
|
||||
|
||||
# An instance of this object is created by the Connection object
|
||||
# for each operation which is requested by the client. If you subclass
|
||||
# Operation, and you override initialize, make sure you call 'super'.
|
||||
|
||||
def initialize(connection, messageID)
|
||||
@connection = connection
|
||||
@respEnvelope = OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::Integer(messageID),
|
||||
# protocolOp,
|
||||
# controls [0] OPTIONAL,
|
||||
])
|
||||
@schema = @connection.opt[:schema]
|
||||
@server = @connection.opt[:server]
|
||||
@attribute_range_limit = @connection.opt[:attribute_range_limit]
|
||||
end
|
||||
|
||||
def log msg, severity = Logger::INFO
|
||||
@connection.log msg, severity
|
||||
end
|
||||
|
||||
def debug msg
|
||||
@connection.debug msg
|
||||
end
|
||||
|
||||
# Send an exception report to the log
|
||||
|
||||
def log_exception msg
|
||||
@connection.log_exception msg
|
||||
end
|
||||
|
||||
##################################################
|
||||
### Utility methods to send protocol responses ###
|
||||
##################################################
|
||||
|
||||
def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
|
||||
@respEnvelope.value[1] = protocolOp
|
||||
if opt[:controls]
|
||||
@respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
|
||||
else
|
||||
@respEnvelope.value.delete_at(2)
|
||||
end
|
||||
|
||||
if false # $debug
|
||||
puts "Response:"
|
||||
p @respEnvelope
|
||||
p @respEnvelope.to_der.unpack("H*")
|
||||
end
|
||||
|
||||
@connection.write(@respEnvelope.to_der)
|
||||
end
|
||||
|
||||
def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
|
||||
seq = [
|
||||
OpenSSL::ASN1::Enumerated(resultCode),
|
||||
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
|
||||
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
|
||||
]
|
||||
if opt[:referral]
|
||||
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
|
||||
seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
yield seq if block_given? # opportunity to add more elements
|
||||
|
||||
send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
|
||||
end
|
||||
|
||||
def send_BindResponse(resultCode, opt={})
|
||||
send_LDAPResult(1, resultCode, opt) do |resp|
|
||||
if opt[:serverSaslCreds]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
AttributeRange = Struct.new :start, :end
|
||||
|
||||
# Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
|
||||
# If schema given, return operational attributes only if
|
||||
# explicitly requested
|
||||
|
||||
def send_SearchResultEntry(dn, avs, opt={})
|
||||
@rescount += 1
|
||||
if @sizelimit
|
||||
raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
|
||||
end
|
||||
|
||||
if @schema
|
||||
# normalize the attribute names
|
||||
@attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s }
|
||||
end
|
||||
|
||||
sendall = @attributes == [] || @attributes.include?("*")
|
||||
avseq = []
|
||||
|
||||
avs.each_with_index do |(attr, vals), aidx|
|
||||
query_attr_idx = @attributes.index(attr)
|
||||
if !query_attr_idx
|
||||
next unless sendall
|
||||
if @schema
|
||||
a = @schema.find_attrtype(attr)
|
||||
next unless a and (a.usage.nil? or a.usage == :userApplications)
|
||||
end
|
||||
end
|
||||
query_attr = query_attr_idx && @attribute_ranges[query_attr_idx]
|
||||
|
||||
if @typesOnly
|
||||
vals = []
|
||||
else
|
||||
vals = [vals] unless vals.kind_of?(Array)
|
||||
# FIXME: optionally do a value_to_s conversion here?
|
||||
# FIXME: handle attribute;binary
|
||||
end
|
||||
|
||||
if (@attribute_range_limit && vals.size > @attribute_range_limit) || query_attr&.start
|
||||
if query_attr&.start
|
||||
range_start = query_attr.start.to_i
|
||||
range_end = query_attr.end == "*" ? -1 : query_attr.end.to_i
|
||||
else
|
||||
range_start = 0
|
||||
range_end = @attribute_range_limit ? @attribute_range_limit - 1 : -1
|
||||
end
|
||||
range_end = range_start + @attribute_range_limit - 1 if @attribute_range_limit && (vals.size - range_start > @attribute_range_limit)
|
||||
range_end = -1 if vals.size <= range_end
|
||||
rvals = vals[range_start .. range_end]
|
||||
vals = []
|
||||
avseq << OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::OctetString("#{attr};range=#{range_start}-#{range_end == -1 ? "*" : range_end}"),
|
||||
OpenSSL::ASN1::Set(rvals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
|
||||
])
|
||||
end
|
||||
|
||||
avseq << OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::OctetString(attr),
|
||||
OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
|
||||
])
|
||||
end
|
||||
|
||||
send_LDAPMessage(OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::OctetString(dn),
|
||||
OpenSSL::ASN1::Sequence(avseq),
|
||||
], 4, :IMPLICIT, :APPLICATION), opt)
|
||||
end
|
||||
|
||||
def send_SearchResultReference(urls, opt={})
|
||||
send_LDAPMessage(OpenSSL::ASN1::Sequence(
|
||||
urls.collect { |url| OpenSSL::ASN1::OctetString(url) }
|
||||
),
|
||||
opt
|
||||
)
|
||||
end
|
||||
|
||||
def send_SearchResultDone(resultCode, opt={})
|
||||
send_LDAPResult(5, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ModifyResponse(resultCode, opt={})
|
||||
send_LDAPResult(7, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_AddResponse(resultCode, opt={})
|
||||
send_LDAPResult(9, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_DelResponse(resultCode, opt={})
|
||||
send_LDAPResult(11, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ModifyDNResponse(resultCode, opt={})
|
||||
send_LDAPResult(13, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_CompareResponse(resultCode, opt={})
|
||||
send_LDAPResult(15, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ExtendedResponse(resultCode, opt={})
|
||||
send_LDAPResult(24, resultCode, opt) do |resp|
|
||||
if opt[:responseName]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
if opt[:response]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##########################################
|
||||
### Methods to parse each request type ###
|
||||
##########################################
|
||||
|
||||
def do_bind(protocolOp, controls) # :nodoc:
|
||||
version = protocolOp.value[0].value
|
||||
dn = protocolOp.value[1].value
|
||||
dn = nil if dn == ""
|
||||
authentication = protocolOp.value[2]
|
||||
|
||||
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
|
||||
when 0
|
||||
simple_bind(version, dn, authentication.value)
|
||||
when 3
|
||||
# mechanism = authentication.value[0].value
|
||||
# credentials = authentication.value[1].value
|
||||
# sasl_bind(version, dn, mechanism, credentials)
|
||||
# FIXME: needs to exchange further BindRequests
|
||||
raise LDAP::ResultError::AuthMethodNotSupported
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
|
||||
end
|
||||
send_BindResponse(0)
|
||||
return dn, version
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_BindResponse(e.to_i, :errorMessage=>e.message)
|
||||
return nil, version
|
||||
end
|
||||
|
||||
# reformat ASN1 into {attr=>[vals], attr=>[vals]}
|
||||
#
|
||||
# AttributeList ::= SEQUENCE OF SEQUENCE {
|
||||
# type AttributeDescription,
|
||||
# vals SET OF AttributeValue }
|
||||
|
||||
def attributelist(set) # :nodoc:
|
||||
av = {}
|
||||
set.value.each do |seq|
|
||||
a = seq.value[0].value
|
||||
if @schema
|
||||
a = @schema.find_attrtype(a).to_s
|
||||
end
|
||||
v = seq.value[1].value.collect { |asn1| asn1.value }
|
||||
# Not clear from the spec whether the same attribute (with
|
||||
# distinct values) can appear more than once in AttributeList
|
||||
raise LDAP::ResultError::AttributeOrValueExists, a if av[a]
|
||||
av[a] = v
|
||||
end
|
||||
return av
|
||||
end
|
||||
|
||||
def do_search(protocolOp, controls) # :nodoc:
|
||||
baseObject = protocolOp.value[0].value
|
||||
scope = protocolOp.value[1].value
|
||||
deref = protocolOp.value[2].value
|
||||
client_sizelimit = protocolOp.value[3].value
|
||||
client_timelimit = protocolOp.value[4].value.to_i
|
||||
@typesOnly = protocolOp.value[5].value
|
||||
filter = Filter::parse(protocolOp.value[6], @schema)
|
||||
attributes = protocolOp.value[7].value.collect {|x| x.value}
|
||||
attributes = attributes.map do |attr|
|
||||
if attr =~ /(.*);range=(\d+)-(\d+|\*)\z/
|
||||
[$1, $2, $3]
|
||||
else
|
||||
attr
|
||||
end
|
||||
end
|
||||
@attributes = attributes.map do |name, |
|
||||
name
|
||||
end
|
||||
@attribute_ranges = attributes.map do |_, range_start, range_end|
|
||||
range_start && AttributeRange.new(range_start, range_end)
|
||||
end
|
||||
|
||||
@rescount = 0
|
||||
@sizelimit = server_sizelimit
|
||||
@sizelimit = client_sizelimit if client_sizelimit > 0 and
|
||||
(@sizelimit.nil? or client_sizelimit < @sizelimit)
|
||||
|
||||
if baseObject.empty? and scope == BaseObject
|
||||
send_SearchResultEntry("", @server.root_dse) if
|
||||
@server.root_dse and LDAP::Server::Filter.run(filter, @server.root_dse)
|
||||
send_SearchResultDone(0)
|
||||
return
|
||||
elsif @schema and baseObject == @schema.subschema_dn
|
||||
send_SearchResultEntry(baseObject, @schema.subschema_subentry) if
|
||||
@schema and @schema.subschema_subentry and
|
||||
LDAP::Server::Filter.run(filter, @schema.subschema_subentry)
|
||||
send_SearchResultDone(0)
|
||||
return
|
||||
end
|
||||
|
||||
t = server_timelimit || 10
|
||||
t = client_timelimit if client_timelimit > 0 and client_timelimit < t
|
||||
|
||||
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do
|
||||
search(baseObject, scope, deref, filter)
|
||||
end
|
||||
send_SearchResultDone(0)
|
||||
|
||||
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
|
||||
rescue LDAP::ResultError => e
|
||||
send_SearchResultDone(e.to_i, :errorMessage=>e.message)
|
||||
|
||||
rescue Abandon
|
||||
# send no response
|
||||
|
||||
# Since this Operation is running in its own thread, we have to
|
||||
# catch all other exceptions. Otherwise, in the event of a programming
|
||||
# error, this thread will silently terminate and the client will wait
|
||||
# forever for a response.
|
||||
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
def do_modify(protocolOp, controls) # :nodoc:
|
||||
dn = protocolOp.value[0].value
|
||||
modinfo = {}
|
||||
protocolOp.value[1].value.each do |seq|
|
||||
attr = seq.value[1].value[0].value
|
||||
if @schema
|
||||
attr = @schema.find_attrtype(attr).to_s
|
||||
end
|
||||
vals = seq.value[1].value[1].value.collect { |v| v.value }
|
||||
case seq.value[0].value.to_i
|
||||
when 0
|
||||
modinfo[attr] = [:add] + vals
|
||||
when 1
|
||||
modinfo[attr] = [:delete] + vals
|
||||
when 2
|
||||
modinfo[attr] = [:replace] + vals
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError, "Bad modify operation #{seq.value[0].value}"
|
||||
end
|
||||
end
|
||||
|
||||
modify(dn, modinfo)
|
||||
send_ModifyResponse(0)
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_ModifyResponse(e.to_i, :errorMessage=>e.message)
|
||||
rescue Abandon
|
||||
# no response
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_ModifyResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
def do_add(protocolOp, controls) # :nodoc:
|
||||
dn = protocolOp.value[0].value
|
||||
av = attributelist(protocolOp.value[1])
|
||||
add(dn, av)
|
||||
send_AddResponse(0)
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_AddResponse(e.to_i, :errorMessage=>e.message)
|
||||
rescue Abandon
|
||||
# no response
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_AddResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
def do_del(protocolOp, controls) # :nodoc:
|
||||
dn = protocolOp.value
|
||||
del(dn)
|
||||
send_DelResponse(0)
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_DelResponse(e.to_i, :errorMessage=>e.message)
|
||||
rescue Abandon
|
||||
# no response
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_DelResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
def do_modifydn(protocolOp, controls) # :nodoc:
|
||||
entry = protocolOp.value[0].value
|
||||
newrdn = protocolOp.value[1].value
|
||||
deleteoldrdn = protocolOp.value[2].value
|
||||
if protocolOp.value.size > 3 and protocolOp.value[3].tag == 0
|
||||
newSuperior = protocolOp.value[3].value
|
||||
end
|
||||
modifydn(entry, newrdn, deleteoldrdn, newSuperior)
|
||||
send_ModifyDNResponse(0)
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_ModifyDNResponse(e.to_i, :errorMessage=>e.message)
|
||||
rescue Abandon
|
||||
# no response
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_ModifyDNResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
def do_compare(protocolOp, controls) # :nodoc:
|
||||
entry = protocolOp.value[0].value
|
||||
ava = protocolOp.value[1].value
|
||||
attr = ava[0].value
|
||||
if @schema
|
||||
attr = @schema.find_attrtype(attr).to_s
|
||||
end
|
||||
val = ava[1].value
|
||||
if compare(entry, attr, val)
|
||||
send_CompareResponse(6) # compareTrue
|
||||
else
|
||||
send_CompareResponse(5) # compareFalse
|
||||
end
|
||||
|
||||
rescue LDAP::ResultError => e
|
||||
send_CompareResponse(e.to_i, :errorMessage=>e.message)
|
||||
rescue Abandon
|
||||
# no response
|
||||
rescue Exception => e
|
||||
log_exception(e)
|
||||
send_CompareResponse(LDAP::ResultCode::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
############################################################
|
||||
### Methods to get parameters related to this connection ###
|
||||
############################################################
|
||||
|
||||
# Server-set maximum time limit. Override for more complex behaviour
|
||||
# (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
|
||||
|
||||
def server_timelimit
|
||||
@connection.opt[:timelimit]
|
||||
end
|
||||
|
||||
# Server-set maximum size limit. Override for more complex behaviour
|
||||
# (e.g. limit depends on @connection.binddn). Return nil for unlimited.
|
||||
|
||||
def server_sizelimit
|
||||
@connection.opt[:sizelimit]
|
||||
end
|
||||
|
||||
######################################################
|
||||
### Methods to actually perform the work requested ###
|
||||
######################################################
|
||||
|
||||
# Handle a simple bind request; raise an exception if the bind is
|
||||
# not acceptable, otherwise just return to accept the bind.
|
||||
#
|
||||
# Override this method in your own subclass.
|
||||
|
||||
def simple_bind(version, dn, password)
|
||||
if version != 3
|
||||
raise LDAP::ResultError::ProtocolError, "version 3 only"
|
||||
end
|
||||
if dn
|
||||
raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
|
||||
end
|
||||
end
|
||||
|
||||
# Handle a search request; override this.
|
||||
#
|
||||
# Call send_SearchResultEntry for each result found. Raise an exception
|
||||
# if there is a problem. timeLimit, sizeLimit and typesOnly are taken
|
||||
# care of, but you need to perform all authorisation checks yourself,
|
||||
# using @connection.binddn
|
||||
|
||||
def search(basedn, scope, deref, filter)
|
||||
debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
|
||||
raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
|
||||
end
|
||||
|
||||
# Handle a modify request; override this
|
||||
#
|
||||
# dn is the object to modify; modification is a hash of
|
||||
# attr => [:add, val, val...] -- add operation
|
||||
# attr => [:replace, val, val...] -- replace operation
|
||||
# attr => [:delete, val, val...] -- delete these values
|
||||
# attr => [:delete] -- delete all values
|
||||
|
||||
def modify(dn, modification)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "modify not implemented"
|
||||
end
|
||||
|
||||
# Handle an add request; override this
|
||||
#
|
||||
# Parameters are the dn of the entry to add, and a hash of
|
||||
# attr=>[val...]
|
||||
# Raise an exception if there is a problem; it is up to you to check
|
||||
# that the connection has sufficient authorisation using @connection.binddn
|
||||
|
||||
def add(dn, av)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "add not implemented"
|
||||
end
|
||||
|
||||
# Handle a del request; override this
|
||||
|
||||
def del(dn)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "delete not implemented"
|
||||
end
|
||||
|
||||
# Handle a modifydn request; override this
|
||||
|
||||
def modifydn(entry, newrdn, deleteoldrdn, newSuperior)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "modifydn not implemented"
|
||||
end
|
||||
|
||||
# Handle a compare request; override this. Return true or false,
|
||||
# or raise an exception for errors.
|
||||
|
||||
def compare(entry, attr, val)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "compare not implemented"
|
||||
end
|
||||
|
||||
end # class Operation
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,92 @@
|
||||
require 'prefork' # <http://raa.ruby-lang.org/project/prefork/>
|
||||
require 'socket'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# Accept connections on a port, and for each one run the given block
|
||||
# in one of N pre-forked children. Returns a Thread object for the
|
||||
# listener.
|
||||
#
|
||||
# Options:
|
||||
# :port=>port number [required]
|
||||
# :bindaddr=>"IP address"
|
||||
# :user=>"username" - drop privileges after bind
|
||||
# :group=>"groupname" - ditto
|
||||
# :logger=>object - implements << method
|
||||
# :listen=>number - listen queue depth
|
||||
# :nodelay=>true - set TCP_NODELAY option
|
||||
# :min_servers=>N - prefork parameters
|
||||
# :max_servers=>N
|
||||
# :max_requests_per_child=>N
|
||||
# :max_idle=>N - seconds
|
||||
|
||||
def self.preforkserver(opt, &blk)
|
||||
server = PreFork.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
|
||||
|
||||
# Drop privileges if requested
|
||||
if opt[:group] or opt[:user]
|
||||
require 'etc'
|
||||
gid = Etc.getgrnam(opt[:group]).gid if opt[:group]
|
||||
uid = Etc.getpwnam(opt[:user]).uid if opt[:user]
|
||||
File.chown(uid, gid, server.instance_eval {@lockf})
|
||||
Process.gid = Process.egid = gid if gid
|
||||
Process.uid = Process.euid = uid if uid
|
||||
end
|
||||
|
||||
# Typically the O/S will buffer response data for 100ms before sending.
|
||||
# If the response is sent as a single write() then there's no need for it.
|
||||
if opt[:nodelay]
|
||||
begin
|
||||
server.sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
||||
rescue Exception
|
||||
end
|
||||
end
|
||||
# set queue size for incoming connections (default is 5)
|
||||
server.sock.listen(opt[:listen]) if opt[:listen]
|
||||
|
||||
# Set prefork server parameters
|
||||
server.min_servers = opt[:min_servers] if opt[:min_servers]
|
||||
server.max_servers = opt[:max_servers] if opt[:max_servers]
|
||||
server.max_request_per_child = opt[:max_request_per_child] if opt[:max_request_per_child]
|
||||
server.max_idle = opt[:max_idle] if opt[:max_idle]
|
||||
|
||||
Thread.new do
|
||||
server.start do |s|
|
||||
begin
|
||||
s.instance_eval(&blk)
|
||||
rescue Interrupt
|
||||
# This exception can be raised to shut the server down
|
||||
server.stop
|
||||
rescue Exception => e
|
||||
opt[:logger].error(s.peeraddr[3]) { "#{e}: #{e.backtrace[0]}" }
|
||||
ensure
|
||||
s.close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
|
||||
if __FILE__ == $0
|
||||
# simple test
|
||||
puts "Running a test POP3 server on port 1110"
|
||||
t = LDAP::Server.preforkserver(:port=>1110) do
|
||||
print "+OK I am a fake POP3 server (pid #{$$})\r\n"
|
||||
while line = gets
|
||||
case line
|
||||
when /^quit/i
|
||||
break
|
||||
when /^crash/i
|
||||
raise Errno::EPERM, "dammit!"
|
||||
else
|
||||
print "-ERR I don't understand #{line}"
|
||||
end
|
||||
end
|
||||
print "+OK bye\r\n"
|
||||
end
|
||||
#sleep 10; t.raise Interrupt # uncomment to run for fixed time period
|
||||
t.join
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
require 'openssl'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
class Request
|
||||
attr_accessor :connection, :typesOnly, :attributes, :rescount, :sizelimit
|
||||
|
||||
# Object to handle a single LDAP request. This object is created on
|
||||
# every request by the router, and is passed as argument to the defined
|
||||
# routes.
|
||||
|
||||
def initialize(connection, messageId)
|
||||
@connection = connection
|
||||
@respEnvelope = OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::Integer(messageId),
|
||||
# protocolOp,
|
||||
# controls [0] OPTIONAL,
|
||||
])
|
||||
@schema = @connection.opt[:schema]
|
||||
@server = @connection.opt[:server]
|
||||
@rescount = 0
|
||||
end
|
||||
|
||||
##################################################
|
||||
### Utility methods to send protocol responses ###
|
||||
##################################################
|
||||
|
||||
def send_LDAPMessage(protocolOp, opt={}) # :nodoc:
|
||||
@respEnvelope.value[1] = protocolOp
|
||||
if opt[:controls]
|
||||
@respEnvelope.value[2] = OpenSSL::ASN1::Set(opt[:controls], 0, :IMPLICIT, APPLICATION)
|
||||
else
|
||||
@respEnvelope.value.delete_at(2)
|
||||
end
|
||||
|
||||
@connection.write(@respEnvelope.to_der)
|
||||
end
|
||||
|
||||
def send_LDAPResult(tag, resultCode, opt={}) # :nodoc:
|
||||
seq = [
|
||||
OpenSSL::ASN1::Enumerated(resultCode),
|
||||
OpenSSL::ASN1::OctetString(opt[:matchedDN] || ""),
|
||||
OpenSSL::ASN1::OctetString(opt[:errorMessage] || ""),
|
||||
]
|
||||
if opt[:referral]
|
||||
rs = opt[:referral].collect { |r| OpenSSL::ASN1::OctetString(r) }
|
||||
seq << OpenSSL::ASN1::Sequence(rs, 3, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
yield seq if block_given? # opportunity to add more elements
|
||||
|
||||
send_LDAPMessage(OpenSSL::ASN1::Sequence(seq, tag, :IMPLICIT, :APPLICATION), opt)
|
||||
end
|
||||
|
||||
def send_BindResponse(resultCode, opt={})
|
||||
send_LDAPResult(1, resultCode, opt) do |resp|
|
||||
if opt[:serverSaslCreds]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:serverSaslCreds], 7, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Send a found entry. Avs are {attr1=>val1, attr2=>[val2,val3]}
|
||||
# If schema given, return operational attributes only if
|
||||
# explicitly requested
|
||||
|
||||
def send_SearchResultEntry(dn, avs, opt={})
|
||||
@rescount += 1
|
||||
if @sizelimit
|
||||
raise LDAP::ResultError::SizeLimitExceeded if @rescount > @sizelimit
|
||||
end
|
||||
|
||||
if @schema
|
||||
# normalize the attribute names
|
||||
@attributes = @attributes.map { |a| a == '*' ? a : @schema.find_attrtype(a).to_s }
|
||||
end
|
||||
|
||||
sendall = @attributes == [] || @attributes.include?("*")
|
||||
avseq = []
|
||||
|
||||
avs.each do |attr, vals|
|
||||
if !@attributes.include?(attr)
|
||||
next unless sendall
|
||||
if @schema
|
||||
a = @schema.find_attrtype(attr)
|
||||
next unless a and (a.usage.nil? or a.usage == :userApplications)
|
||||
end
|
||||
end
|
||||
|
||||
if @typesOnly
|
||||
vals = []
|
||||
else
|
||||
vals = [vals] unless vals.kind_of?(Array)
|
||||
# FIXME: optionally do a value_to_s conversion here?
|
||||
# FIXME: handle attribute;binary
|
||||
end
|
||||
|
||||
avseq << OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::OctetString(attr),
|
||||
OpenSSL::ASN1::Set(vals.collect { |v| OpenSSL::ASN1::OctetString(v.to_s) })
|
||||
])
|
||||
end
|
||||
|
||||
send_LDAPMessage(OpenSSL::ASN1::Sequence([
|
||||
OpenSSL::ASN1::OctetString(dn),
|
||||
OpenSSL::ASN1::Sequence(avseq),
|
||||
], 4, :IMPLICIT, :APPLICATION), opt)
|
||||
end
|
||||
|
||||
def send_SearchResultDone(resultCode, opt={})
|
||||
send_LDAPResult(5, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ModifyResponse(resultCode, opt={})
|
||||
send_LDAPResult(7, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_AddResponse(resultCode, opt={})
|
||||
send_LDAPResult(9, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_DelResponse(resultCode, opt={})
|
||||
send_LDAPResult(11, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ModifyDNResponse(resultCode, opt={})
|
||||
send_LDAPResult(13, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_CompareResponse(resultCode, opt={})
|
||||
send_LDAPResult(15, resultCode, opt)
|
||||
end
|
||||
|
||||
def send_ExtendedResponse(resultCode, opt={})
|
||||
send_LDAPResult(24, resultCode, opt) do |resp|
|
||||
if opt[:responseName]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:responseName], 10, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
if opt[:response]
|
||||
resp << OpenSSL::ASN1::OctetString(opt[:response], 11, :IMPLICIT, :APPLICATION)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
############################################################
|
||||
### Methods to get parameters related to this connection ###
|
||||
############################################################
|
||||
|
||||
# Server-set maximum time limit. Override for more complex behaviour
|
||||
# (e.g. limit depends on @connection.binddn). Nil uses hardcoded default.
|
||||
|
||||
def server_timelimit
|
||||
@connection.opt[:timelimit]
|
||||
end
|
||||
|
||||
# Server-set maximum size limit. Override for more complex behaviour
|
||||
# (e.g. limit depends on @connection.binddn). Return nil for unlimited.
|
||||
|
||||
def server_sizelimit
|
||||
@connection.opt[:sizelimit]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
module LDAP
|
||||
|
||||
# compatible with ruby-ldap
|
||||
class Error < StandardError
|
||||
end
|
||||
|
||||
class ResultError < Error
|
||||
end
|
||||
|
||||
# This exception is raised when we need to kill an existing Operation
|
||||
# thread because of a received abandonRequest or bindRequest
|
||||
class Abandon < Interrupt
|
||||
end
|
||||
|
||||
# ResultError constants from RFC 2251 4.1.10; these are all exceptions
|
||||
# which can be raised
|
||||
|
||||
class ResultError
|
||||
class Success < self; def to_i; 0; end; end
|
||||
class OperationsError < self; def to_i; 1; end; end
|
||||
class ProtocolError < self; def to_i; 2; end; end
|
||||
class TimeLimitExceeded < self; def to_i; 3; end; end
|
||||
class SizeLimitExceeded < self; def to_i; 4; end; end
|
||||
class CompareFalse < self; def to_i; 5; end; end
|
||||
class CompareTrue < self; def to_i; 6; end; end
|
||||
class AuthMethodNotSupported < self; def to_i; 7; end; end
|
||||
class StrongAuthRequired < self; def to_i; 8; end; end
|
||||
class Referral < self; def to_i; 10; end; end
|
||||
class AdminLimitExceeded < self; def to_i; 11; end; end
|
||||
class UnavailableCriticalExtension < self; def to_i; 12; end; end
|
||||
class ConfidentialityRequired < self; def to_i; 13; end; end
|
||||
class SaslBindInProgress < self; def to_i; 14; end; end
|
||||
class NoSuchAttribute < self; def to_i; 16; end; end
|
||||
class UndefinedAttributeType < self; def to_i; 17; end; end
|
||||
class InappropriateMatching < self; def to_i; 18; end; end
|
||||
class ConstraintViolation < self; def to_i; 19; end; end
|
||||
class AttributeOrValueExists < self; def to_i; 20; end; end
|
||||
class InvalidAttributeSyntax < self; def to_i; 21; end; end
|
||||
class NoSuchObject < self; def to_i; 32; end; end
|
||||
class AliasProblem < self; def to_i; 33; end; end
|
||||
class InvalidDNSyntax < self; def to_i; 34; end; end
|
||||
class IsLeaf < self; def to_i; 35; end; end
|
||||
class AliasDereferencingProblem < self; def to_i; 36; end; end
|
||||
class InappropriateAuthentication < self; def to_i; 48; end; end
|
||||
class InvalidCredentials < self; def to_i; 49; end; end
|
||||
class InsufficientAccessRights < self; def to_i; 50; end; end
|
||||
class Busy < self; def to_i; 51; end; end
|
||||
class Unavailable < self; def to_i; 52; end; end
|
||||
class UnwillingToPerform < self; def to_i; 53; end; end
|
||||
class LoopDetect < self; def to_i; 54; end; end
|
||||
class NamingViolation < self; def to_i; 64; end; end
|
||||
class ObjectClassViolation < self; def to_i; 65; end; end
|
||||
class NotAllowedOnNonLeaf < self; def to_i; 66; end; end
|
||||
class NotAllowedOnRDN < self; def to_i; 67; end; end
|
||||
class EntryAlreadyExists < self; def to_i; 68; end; end
|
||||
class ObjectClassModsProhibited < self; def to_i; 69; end; end
|
||||
class AffectsMultipleDSAs < self; def to_i; 71; end; end
|
||||
class Other < self; def to_i; 80; end; end
|
||||
|
||||
# Reverse lookup: so you can do raise LDAP::ResultError[53]
|
||||
|
||||
N_TO_CLASS = {
|
||||
53 => UnwillingToPerform,
|
||||
# FIXME: please fill in the rest
|
||||
}
|
||||
def self.[](n)
|
||||
return N_TO_CLASS[n] || self
|
||||
end
|
||||
end # class ResultError
|
||||
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,220 @@
|
||||
require 'ldap/server/dn'
|
||||
require 'ldap/server/util'
|
||||
require 'ldap/server/trie'
|
||||
require 'ldap/server/request'
|
||||
require 'ldap/server/filter'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
class Router
|
||||
@logger
|
||||
@routes
|
||||
|
||||
# Scope
|
||||
BaseObject = 0
|
||||
SingleLevel = 1
|
||||
WholeSubtree = 2
|
||||
|
||||
# DerefAliases
|
||||
NeverDerefAliases = 0
|
||||
DerefInSearching = 1
|
||||
DerefFindingBaseObj = 2
|
||||
DerefAlways = 3
|
||||
|
||||
def initialize(logger, &block)
|
||||
@logger = logger
|
||||
|
||||
@routes = Hash.new
|
||||
@routes = Trie.new do |trie|
|
||||
# Add an artificial LDAP component
|
||||
trie << "op=bind"
|
||||
trie << "op=search"
|
||||
end
|
||||
|
||||
self.instance_eval(&block)
|
||||
end
|
||||
|
||||
def log_exception(e, level = :error)
|
||||
@logger.send level, e.message
|
||||
e.backtrace.each { |line| @logger.send level, "\tfrom#{line}" } if e.backtrace
|
||||
end
|
||||
|
||||
######################
|
||||
### Initialization ###
|
||||
######################
|
||||
|
||||
def route(operation, hash)
|
||||
hash.each do |key, value|
|
||||
if key.nil?
|
||||
@routes.insert "op=#{operation.to_s}", value
|
||||
@logger.debug "map operation #{operation.to_s} all routes to #{value}"
|
||||
else
|
||||
@routes.insert "#{key},op=#{operation.to_s}", value
|
||||
@logger.debug "map #{operation.to_s} #{key} to #{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
if [:bind, :search, :add, :modify, :modifydn, :del, :compare].include? name
|
||||
send :route, name, *args
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
####################################################
|
||||
### Methods to parse and route each request type ###
|
||||
####################################################
|
||||
def parse_route(dn, method)
|
||||
route, action = @routes.match("#{dn},op=#{method.to_s}")
|
||||
if not route or route.empty?
|
||||
@logger.warn "No route defined for \'#{route}\'"
|
||||
raise LDAP::ResultError::UnwillingToPerform
|
||||
end
|
||||
if action.nil?
|
||||
@logger.error "No action defined for route \'#{route}\'"
|
||||
raise LDAP::ResultError::UnwillingToPerform
|
||||
end
|
||||
|
||||
class_name = action.split('#').first
|
||||
method_name = action.split('#').last
|
||||
|
||||
params = LDAP::Server::DN.new("#{dn},op=#{method.to_s}").parse(route)
|
||||
|
||||
return class_name, method_name, params
|
||||
end
|
||||
|
||||
def do_bind(connection, messageId, protocolOp, controls) # :nodoc:
|
||||
request = Request.new(connection, messageId)
|
||||
version = protocolOp.value[0].value
|
||||
dn = protocolOp.value[1].value
|
||||
dn = nil if dn.empty?
|
||||
authentication = protocolOp.value[2]
|
||||
|
||||
@logger.debug "subject:#{connection.binddn} predicate:bind object:#{dn}"
|
||||
|
||||
# Find a route in the routing tree
|
||||
class_name, method_name, params = parse_route(dn, :bind)
|
||||
|
||||
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
|
||||
when 0
|
||||
Object.const_get(class_name).send method_name, request, version, dn, authentication.value, params
|
||||
when 3
|
||||
mechanism = authentication.value[0].value
|
||||
credentials = authentication.value[1].value
|
||||
# sasl_bind(version, dn, mechanism, credentials)
|
||||
# FIXME: needs to exchange further BindRequests
|
||||
# route_sasl_bind(request, version, dn, mechanism, credentials)
|
||||
raise LDAP::ResultError::AuthMethodNotSupported
|
||||
else
|
||||
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
|
||||
end
|
||||
request.send_BindResponse(0)
|
||||
return dn, version
|
||||
rescue NoMethodError => e
|
||||
log_exception e
|
||||
request.send_BindResponse(LDAP::ResultError::OperationsError.new.to_i, :errorMessage => e.message)
|
||||
return nil, version
|
||||
rescue LDAP::ResultError => e
|
||||
log_exception e
|
||||
request.send_BindResponse(e.to_i, :errorMessage => e.message)
|
||||
return nil, version
|
||||
end
|
||||
|
||||
def do_search(connection, messageId, protocolOp, controls) # :nodoc:
|
||||
request = Request.new(connection, messageId)
|
||||
server = connection.opt[:server]
|
||||
schema = connection.opt[:schema]
|
||||
baseObject = protocolOp.value[0].value
|
||||
scope = protocolOp.value[1].value
|
||||
deref = protocolOp.value[2].value
|
||||
client_sizelimit = protocolOp.value[3].value
|
||||
client_timelimit = protocolOp.value[4].value.to_i
|
||||
request.typesOnly = protocolOp.value[5].value
|
||||
filter = LDAP::Server::Filter::parse(protocolOp.value[6], schema)
|
||||
request.attributes = protocolOp.value[7].value.collect {|x| x.value}
|
||||
|
||||
sizelimit = request.server_sizelimit
|
||||
sizelimit = client_sizelimit if client_sizelimit > 0 and
|
||||
(sizelimit.nil? or client_sizelimit < sizelimit)
|
||||
request.sizelimit = sizelimit
|
||||
|
||||
if baseObject.empty? and scope == BaseObject
|
||||
request.send_SearchResultEntry("", server.root_dse) if
|
||||
server.root_dse and LDAP::Server::Filter.run(filter, server.root_dse)
|
||||
request.send_SearchResultDone(0)
|
||||
return
|
||||
elsif schema and baseObject == schema.subschema_dn
|
||||
request.send_SearchResultEntry(baseObject, schema.subschema_subentry) if
|
||||
schema and schema.subschema_subentry and
|
||||
LDAP::Server::Filter.run(filter, schema.subschema_subentry)
|
||||
request.send_SearchResultDone(0)
|
||||
return
|
||||
end
|
||||
|
||||
t = request.server_timelimit || 10
|
||||
t = client_timelimit if client_timelimit > 0 and client_timelimit < t
|
||||
|
||||
@logger.debug "subject:#{connection.binddn} predicate:search object:#{baseObject}"
|
||||
|
||||
# Find a route in the routing tree
|
||||
class_name, method_name, params = parse_route(baseObject, :search)
|
||||
|
||||
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do
|
||||
Object.const_get(class_name).send method_name, request, baseObject, scope, deref, filter, params
|
||||
end
|
||||
request.send_SearchResultDone(0)
|
||||
|
||||
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
|
||||
rescue LDAP::ResultError => e
|
||||
request.send_SearchResultDone(e.to_i, :errorMessage=>e.message)
|
||||
|
||||
rescue Abandon
|
||||
# send no response
|
||||
|
||||
# Since this Operation is running in its own thread, we have to
|
||||
# catch all other exceptions. Otherwise, in the event of a programming
|
||||
# error, this thread will silently terminate and the client will wait
|
||||
# forever for a response.
|
||||
|
||||
rescue Exception => e
|
||||
log_exception e
|
||||
request.send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
|
||||
end
|
||||
|
||||
|
||||
###########################################################
|
||||
### Methods to actually perform the work requested ###
|
||||
### Use the signatures below to write your own handlers ###
|
||||
###########################################################
|
||||
|
||||
# Handle a simple bind request; raise an exception if the bind is
|
||||
# not acceptable, otherwise just return to accept the bind.
|
||||
#
|
||||
# Write your own class method using this signature
|
||||
|
||||
# def simple_bind(request, version, dn, password, params)
|
||||
# if version != 3
|
||||
# raise LDAP::ResultError::ProtocolError, "version 3 only"
|
||||
# end
|
||||
# if dn
|
||||
# raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
|
||||
# end
|
||||
# end
|
||||
|
||||
# Handle a search request
|
||||
#
|
||||
# Call request. send_SearchResultEntry for each result found. Raise
|
||||
# an exception if there is a problem. timeLimit, sizeLimit and
|
||||
# typesOnly are taken care of, but you need to perform all
|
||||
# authorisation checks yourself, using @connection.binddn
|
||||
|
||||
# def search(basedn, scope, deref, filter)
|
||||
# debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
|
||||
# raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
|
||||
# end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,592 @@
|
||||
require 'ldap/server/syntax'
|
||||
require 'ldap/server/result'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# This object represents an LDAP schema: that is, a collection of
|
||||
# objectclasses and attributetypes. Methods are provided for loading
|
||||
# the schema (from a string or a disk file), and validating an av-hash
|
||||
# against it.
|
||||
|
||||
class Schema
|
||||
|
||||
SUBSCHEMA_ENTRY_ATTR = 'cn'
|
||||
SUBSCHEMA_ENTRY_VALUE = 'Subschema'
|
||||
|
||||
def initialize
|
||||
@attrtypes = {} # name/alias/oid => AttributeType instance
|
||||
@objectclasses = {} # name/alias/oid => ObjectClass instance
|
||||
@subschema_cache = nil
|
||||
end
|
||||
|
||||
# return the DN of the subschema subentry
|
||||
|
||||
def subschema_dn
|
||||
"#{SUBSCHEMA_ENTRY_ATTR}=#{SUBSCHEMA_ENTRY_VALUE}"
|
||||
end
|
||||
|
||||
# Return an av hash object giving the subschema subentry. This is cached, so
|
||||
# call Schema#changed if it needs to be rebuilt
|
||||
|
||||
def subschema_subentry
|
||||
@subschema_cache ||= {
|
||||
'objectClass' => ['top','subschema','extensibleObject'],
|
||||
SUBSCHEMA_ENTRY_ATTR => [SUBSCHEMA_ENTRY_VALUE],
|
||||
'objectClasses' => all_objectclasses.collect { |s| s.to_def },
|
||||
'attributeTypes' => all_attrtypes.collect { |s| s.to_def },
|
||||
'ldapSyntaxes' => LDAP::Server::Syntax.all_syntaxes.collect { |s| s.to_def },
|
||||
#'matchingRules' =>
|
||||
#'matchingRuleUse' =>
|
||||
}
|
||||
end
|
||||
|
||||
# Clear the subschema subentry cache, so the next time someone requests
|
||||
# it, it will be rebuilt
|
||||
|
||||
def changed
|
||||
@subschema_cache = nil
|
||||
end
|
||||
|
||||
# Add an AttributeType to the schema
|
||||
|
||||
def add_attrtype(str)
|
||||
a = AttributeType.new(str)
|
||||
@attrtypes[a.oid] = a if a.oid
|
||||
a.names.each do |n|
|
||||
@attrtypes[n.downcase] = a
|
||||
end
|
||||
end
|
||||
|
||||
# Locate an attributetype object by name/alias/oid (or raise exception)
|
||||
|
||||
def find_attrtype(n)
|
||||
return n if n.nil? or n.is_a?(LDAP::Server::Schema::AttributeType)
|
||||
r = @attrtypes[n.downcase]
|
||||
raise LDAP::ResultError::UndefinedAttributeType, "Unknown AttributeType #{n.inspect}" unless r
|
||||
r
|
||||
end
|
||||
|
||||
# Return array of all AttributeType objects in this schema
|
||||
|
||||
def all_attrtypes
|
||||
@attrtypes.values.uniq
|
||||
end
|
||||
|
||||
# Add an ObjectClass to the schema
|
||||
|
||||
def add_objectclass(str)
|
||||
o = ObjectClass.new(str)
|
||||
@objectclasses[o.oid] = o if o.oid
|
||||
o.names.each do |n|
|
||||
@objectclasses[n.downcase] = o
|
||||
end
|
||||
end
|
||||
|
||||
# Locate an objectclass object by name/alias/oid (or raise exception)
|
||||
|
||||
def find_objectclass(n)
|
||||
return n if n.nil? or n.is_a?(LDAP::Server::Schema::ObjectClass)
|
||||
r = @objectclasses[n.downcase]
|
||||
raise LDAP::ResultError::ObjectClassViolation, "Unknown ObjectClass #{n.inspect}" unless r
|
||||
r
|
||||
end
|
||||
|
||||
# Return array of all ObjectClass objects in this schema
|
||||
|
||||
def all_objectclasses
|
||||
@objectclasses.values.uniq
|
||||
end
|
||||
|
||||
# Load an OpenLDAP-format schema from a named file (see notes under 'load')
|
||||
|
||||
def load_file(filename)
|
||||
File.open(filename) { |f| load(f) }
|
||||
end
|
||||
|
||||
# Load an OpenLDAP-format schema from a string or IO object (anything
|
||||
# which responds to 'each_line'). Lines starting 'attributetype'
|
||||
# or 'objectclass' contain one of those objects. Does not implement
|
||||
# named objectIdentifier prefixes (used in the dyngroup.schema file
|
||||
# supplied with openldap, but not documented in RFC2252)
|
||||
#
|
||||
# Note: RFC2252 is strict about the order in which the elements appear,
|
||||
# and so are we, but OpenLDAP is not. This means that a schema which
|
||||
# works in OpenLDAP might not load here. For example, RFC2252 says
|
||||
# that in an objectclass description, "SUP" must come before "MAY";
|
||||
# if they are the other way round, our regexp-based parser will not
|
||||
# accept it. The solution is simply to modify the definition so that
|
||||
# the elements appear in the correct order.
|
||||
|
||||
def load(str_or_io)
|
||||
meth = :junk_line
|
||||
data = ""
|
||||
str_or_io.each_line do |line|
|
||||
case line
|
||||
when /^\s*#/, /^\s*$/
|
||||
next
|
||||
when /^objectclass\s*(.*)$/i
|
||||
m = $~
|
||||
send(meth, data)
|
||||
meth, data = :add_objectclass, m[1]
|
||||
when /^attributetype\s*(.*)$/i
|
||||
m = $~
|
||||
send(meth, data)
|
||||
meth, data = :add_attrtype, m[1]
|
||||
else
|
||||
data << line
|
||||
end
|
||||
end
|
||||
send(meth,data)
|
||||
self
|
||||
end
|
||||
|
||||
def junk_line(data)
|
||||
return if data.empty?
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Expected 'attributetype' or 'objectclass', got #{data}"
|
||||
end
|
||||
private :junk_line
|
||||
|
||||
# Load in the base set of objectclasses and attributetypes, being
|
||||
# the same set as OpenLDAP preloads internally. Includes objectclasses
|
||||
# 'top', 'objectclass'; attributetypes 'objectclass' , 'cn',
|
||||
# 'userPassword' and 'distinguishedName'; common operational attributes
|
||||
# such as 'modifyTimestamp'; plus extras needed for publishing a v3
|
||||
# schema via LDAP
|
||||
|
||||
def load_system
|
||||
load(<<EOS)
|
||||
attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
|
||||
attributetype ( 2.5.4.35 NAME 'userPassword' DESC 'RFC2256/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )
|
||||
attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC2256: common name(s) for which the entity is known by' SUP name )
|
||||
attributetype ( 2.5.4.41 NAME 'name' DESC 'RFC2256: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
|
||||
attributetype ( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC2256: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
|
||||
attributetype ( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'namedref: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation )
|
||||
attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC2256: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC2252: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC2252: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.6 NAME 'objectClasses' DESC 'RFC2252: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC2252: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.4 NAME 'matchingRules' DESC 'RFC2252: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )
|
||||
attributetype ( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC2252: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC2252: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC2252: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC2252: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC2252: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )
|
||||
attributetype ( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC2252: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )
|
||||
attributetype ( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC2252: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.18.4 NAME 'modifiersName' DESC 'RFC2252: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.18.3 NAME 'creatorsName' DESC 'RFC2252: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC2252: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC2252: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.9 NAME 'structuralObjectClass' DESC 'X.500(93): structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
|
||||
attributetype ( 2.5.4.0 NAME 'objectClass' DESC 'RFC2256: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
|
||||
# These ones aren't published by OpenLDAP, but are referenced by the 'subschema' objectclass
|
||||
attributetype ( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation )
|
||||
attributetype ( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation )
|
||||
|
||||
objectclass ( 2.5.20.1 NAME 'subschema' DESC 'RFC2252: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )
|
||||
#Don't have definition for subtreeSpecification:
|
||||
#objectClass ( 2.5.17.0 NAME 'subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )
|
||||
objectClass ( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )
|
||||
objectClass ( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref )
|
||||
objectClass ( 2.5.6.1 NAME 'alias' DESC 'RFC2256: an alias' SUP top STRUCTURAL MUST aliasedObjectName )
|
||||
objectClass ( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC2252: extensible object' SUP top AUXILIARY )
|
||||
objectClass ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
|
||||
EOS
|
||||
end
|
||||
|
||||
# After loading object classes and attr types: resolve oid strings to point
|
||||
# to objects. This will expose schema inconsistencies (e.g. objectclass
|
||||
# has unknown SUP class or points to unknown attributeType). However,
|
||||
# unknown Syntaxes just create new Syntax objects.
|
||||
|
||||
def resolve_oids
|
||||
|
||||
all_attrtypes.each do |a|
|
||||
if a.sup
|
||||
s = find_attrtype(a.sup)
|
||||
a.instance_eval {
|
||||
@sup = s
|
||||
# inherit properties (FIXME: This breaks to_def)
|
||||
@equality ||= s.equality
|
||||
@ordering ||= s.ordering
|
||||
@substr ||= s.substr
|
||||
@syntax ||= s.syntax
|
||||
@maxlen ||= s.maxlen
|
||||
@singlevalue ||= s.singlevalue
|
||||
@collective ||= s.collective
|
||||
@nousermod ||= s.nousermod
|
||||
@usage ||= s.usage
|
||||
}
|
||||
end
|
||||
a.instance_eval do
|
||||
@syntax = LDAP::Server::Syntax.find(@syntax) if @syntax
|
||||
@equality = LDAP::Server::MatchingRule.find(@equality) if @equality
|
||||
@ordering = LDAP::Server::MatchingRule.find(@ordering) if @ordering
|
||||
@substr = LDAP::Server::MatchingRule.find(@substr) if @substr
|
||||
end
|
||||
end
|
||||
|
||||
all_objectclasses.each do |o|
|
||||
if o.sup
|
||||
s = o.sup.collect { |ss| find_objectclass(ss) }
|
||||
o.instance_eval { @sup = s }
|
||||
end
|
||||
if o.must
|
||||
s = o.must.collect { |ss| find_attrtype(ss) }
|
||||
o.instance_eval { @must = s }
|
||||
end
|
||||
if o.may
|
||||
s = o.may.collect { |ss| find_attrtype(ss) }
|
||||
o.instance_eval { @may = s }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Validate a new entry or update. For a new entry, just pass a hash
|
||||
# of attr=>[val, val, ...]; for an update, the first parameter is
|
||||
# a hash of attr=>[:modtype, val, val...] and the second parameter
|
||||
# is the existing entry, where it is assumed that the attribute names
|
||||
# are already in their standard string forms (as returned by attr#name)
|
||||
#
|
||||
# Returns a hash containing the updated entry.
|
||||
#
|
||||
# If a block is given, it is called to decide whether the user is
|
||||
# allowed to update an attribute; parameter is the attr *object*
|
||||
# (not name; use #name if you need its name instead). Return false
|
||||
# if the update is not permitted. Otherwise, the only restriction
|
||||
# will be that updates to attributes declared 'nousermod' are forbidden.
|
||||
#
|
||||
# No DN checks are done here, since we don't know the DN.
|
||||
# Checking that the entry contains an attribute for the RDN is the
|
||||
# responsibility of the caller.
|
||||
|
||||
def validate(mods, entry={})
|
||||
|
||||
# Run through the mods, make the normalized names, and perform any
|
||||
# updates
|
||||
|
||||
# FIXME: I don't know if these are the right results to return
|
||||
# for the various types of validation errors
|
||||
|
||||
oc_changed = false
|
||||
res = entry.dup
|
||||
mods.each do |attrname, nv|
|
||||
attr = find_attrtype(attrname)
|
||||
attrname = attr.to_s
|
||||
raise LDAP::ResultError::ConstraintViolation,
|
||||
"Cannot modify #{attrname}" if attr.nousermod or
|
||||
(block_given? and !yield(attr))
|
||||
# Perform the update
|
||||
vals = res[attrname] || []
|
||||
checkvals = []
|
||||
nv = [nv] unless nv.is_a?(Array)
|
||||
|
||||
case nv.first
|
||||
when :add
|
||||
checkvals = nv[1..-1]
|
||||
vals += checkvals
|
||||
vals.uniq! # FIXME: ?? error if duplicate values
|
||||
# FIXME: normalize values? e.g. c: gb and c: GB are same value.
|
||||
when :delete
|
||||
nv = nv[1..-1]
|
||||
if nv.empty?
|
||||
vals = [] # ?? error if does not exist
|
||||
else
|
||||
nv.each { |v| vals.delete(v) } # ?? error if value missing
|
||||
end
|
||||
when :replace
|
||||
vals = checkvals = nv[1..-1]
|
||||
else
|
||||
vals = checkvals = nv
|
||||
end
|
||||
if vals == []
|
||||
res.delete(attrname)
|
||||
else
|
||||
res[attrname] = vals
|
||||
end
|
||||
|
||||
# Attribute validation
|
||||
raise LDAP::ResultError::ObjectClassViolation,
|
||||
"Attribute #{attr} is SINGLE-VALUE" if attr.singlevalue and vals.size > 1
|
||||
|
||||
checkvals.each do |val|
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Nil or empty value for attribute #{attr}" if val.nil? or val.empty?
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Bad value for #{attr}: #{val.inspect}" if attr.syntax and ! attr.syntax.match(val)
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Value too long for #{attr} (max #{attr.maxlen})" if attr.maxlen and val.length > attr.maxlen
|
||||
end
|
||||
|
||||
oc_changed = true if attrname == 'objectClass'
|
||||
end
|
||||
|
||||
# Now do objectClass checks
|
||||
oc = res['objectClass']
|
||||
unless oc
|
||||
raise LDAP::ResultError::ObjectClassViolation,
|
||||
"objectClass attribute missing"
|
||||
end
|
||||
oc = oc.collect { |val| find_objectclass(val) }
|
||||
|
||||
if oc_changed
|
||||
# Add superior objectClasses (note: growing an array while you
|
||||
# iterate over it seems to work, in ruby-1.8.2 anyway!)
|
||||
oc.each do |objectclass|
|
||||
objectclass.sup.each do |s|
|
||||
oc.push(s) unless oc.include?(s)
|
||||
end
|
||||
end
|
||||
res['objectClass'] = oc.collect { |oo| oo.to_s }
|
||||
|
||||
# Check that exactly one structural objectClass is present
|
||||
unless oc.find_all { |s| s.struct == :structural }.size >= 1
|
||||
raise LDAP::ResultError::ObjectClassViolation,
|
||||
"Entry must have at least one structural objectClass"
|
||||
# Exactly one? But you have to sort out the inheritance problem
|
||||
# (e.g. both person and organizationalPerson are declared
|
||||
# structural)
|
||||
end
|
||||
end
|
||||
|
||||
# Ensure that all MUST attributes are present
|
||||
allow_attr = {}
|
||||
oc.each do |objectclass|
|
||||
objectclass.must.each do |m|
|
||||
unless res[m.name] and res[m.name] != []
|
||||
raise LDAP::ResultError::ObjectClassViolation, "Missing attribute #{m} required by objectClass #{objectclass}"
|
||||
end
|
||||
allow_attr[m.name] = true
|
||||
end
|
||||
objectclass.may.each do |m|
|
||||
allow_attr[m.name] = true
|
||||
end
|
||||
end
|
||||
|
||||
unless oc.find { |objectclass| objectclass.name == 'extensibleObject' }
|
||||
# Now check all the attributes given are permitted by MUST or MAY
|
||||
res.each_key do |attr|
|
||||
unless allow_attr[attr] or find_attrtype(attr).usage == :directoryOperation
|
||||
raise LDAP::ResultError::ObjectClassViolation, "Attribute #{attr} not permitted by objectClass"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
# Hopefully backwards-compatible API for ruby-ldap's LDAP::Schema.
|
||||
# Since MUST/MAY/SUP may point to schema objects, convert them back
|
||||
# to strings.
|
||||
|
||||
def names(key)
|
||||
case key
|
||||
when 'objectClasses'
|
||||
return all_objectclasses.collect { |e| e.name }
|
||||
when 'attributeTypes'
|
||||
return all_attrtypes.collect { |e| e.name }
|
||||
when 'ldapSyntaxes'
|
||||
return LDAP::Server::Syntax.all_syntaxes.collect { |e| e.name }
|
||||
when 'matchingRules'
|
||||
return LDAP::Server::MatchingRule.all_matching_rules.collect { |e| e.name }
|
||||
# TODO: matchingRuleUse
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
# Backwards-compatible for ruby-ldap LDAP::Schema
|
||||
|
||||
def attr(oc,at)
|
||||
o = find_objectclass(oc)
|
||||
case at.upcase
|
||||
when 'MUST'
|
||||
return o.must.collect { |e| e.to_s }
|
||||
when 'MAY'
|
||||
return o.may.collect { |e| e.to_s }
|
||||
when 'SUP'
|
||||
return o.sup.collect { |e| e.to_s }
|
||||
when 'NAME'
|
||||
return o.names.collect { |e| e.to_s }
|
||||
when 'DESC'
|
||||
return [o.desc]
|
||||
end
|
||||
return nil
|
||||
rescue LDAP::ResultError
|
||||
return nil
|
||||
end
|
||||
|
||||
# Backwards-compatible for ruby-ldap LDAP::Schema
|
||||
|
||||
def must(oc)
|
||||
attr(oc, "MUST")
|
||||
end
|
||||
|
||||
# Backwards-compatible for ruby-ldap LDAP::Schema
|
||||
|
||||
def may(oc)
|
||||
attr(oc, "MAY")
|
||||
end
|
||||
|
||||
# Backwards-compatible for ruby-ldap LDAP::Schema
|
||||
|
||||
def sup(oc)
|
||||
attr(oc, "SUP")
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
|
||||
# Class holding an instance of an AttributeTypeDescription (RFC2252 4.2)
|
||||
|
||||
class AttributeType
|
||||
|
||||
attr_reader :oid, :names, :desc, :obsolete, :sup, :equality, :ordering
|
||||
attr_reader :substr, :syntax, :maxlen, :singlevalue, :collective
|
||||
attr_reader :nousermod, :usage
|
||||
|
||||
def initialize(str)
|
||||
m = LDAP::Server::Syntax::AttributeTypeDescription.match(str)
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Bad AttributeTypeDescription #{str.inspect}" unless m
|
||||
@oid = m[1]
|
||||
@names = (m[2]||"").scan(/'(.*?)'/).flatten
|
||||
@desc = m[3]
|
||||
@obsolete = ! m[4].nil?
|
||||
@sup = m[5]
|
||||
@equality = m[6]
|
||||
@ordering = m[7]
|
||||
@substr = m[8]
|
||||
@syntax = m[9]
|
||||
@maxlen = m[10] && m[10].to_i
|
||||
@singlevalue = ! m[11].nil?
|
||||
@collective = ! m[12].nil?
|
||||
@nousermod = ! m[13].nil?
|
||||
@usage = m[14] && m[14].intern
|
||||
# This is the cache of the stringified version. Rather than
|
||||
# initialize to str, we set nil to force it to be rebuilt
|
||||
@def = nil
|
||||
end
|
||||
|
||||
def name
|
||||
@names.first
|
||||
end
|
||||
|
||||
def to_s
|
||||
(@names && @names.first) || @oid
|
||||
end
|
||||
|
||||
def changed
|
||||
@def = nil
|
||||
end
|
||||
|
||||
def to_def
|
||||
return @def if @def
|
||||
ans = "( #{@oid} "
|
||||
if @names.nil? or @names.empty?
|
||||
# nothing
|
||||
elsif @names.size == 1
|
||||
ans << "NAME '#{@names.first}' "
|
||||
else
|
||||
ans << "NAME ( "
|
||||
@names.each { |n| ans << "'#{n}' " }
|
||||
ans << ") "
|
||||
end
|
||||
ans << "DESC '#{@desc}' " if @desc
|
||||
ans << "OBSOLETE " if @obsolete
|
||||
ans << "SUP #{@sup} " if @sup # oid
|
||||
ans << "EQUALITY #{@equality} " if @equality # oid
|
||||
ans << "ORDERING #{@ordering} " if @ordering # oid
|
||||
ans << "SUBSTR #{@substr} " if @substr # oid
|
||||
ans << "SYNTAX #{@syntax}#{@maxlen && "{#{@maxlen}}"} " if @syntax
|
||||
ans << "SINGLE-VALUE " if @singlevalue
|
||||
ans << "COLLECTIVE " if @collective
|
||||
ans << "NO-USER-MODIFICATION " if @nousermod
|
||||
ans << "USAGE #{@usage} " if @usage
|
||||
ans << ")"
|
||||
@def = ans
|
||||
end
|
||||
end # class AttributeType
|
||||
|
||||
#####################################################################
|
||||
|
||||
# Class holding an instance of an ObjectClassDescription (RFC2252 4.4)
|
||||
|
||||
class ObjectClass
|
||||
|
||||
attr_reader :oid, :names, :desc, :obsolete, :sup, :struct, :must, :may
|
||||
|
||||
SCAN_WOID = /#{LDAP::Server::Syntax::WOID}/x
|
||||
|
||||
def initialize(str)
|
||||
m = LDAP::Server::Syntax::ObjectClassDescription.match(str)
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Bad ObjectClassDescription #{str.inspect}" unless m
|
||||
@oid = m[1]
|
||||
@names = (m[2]||"").scan(/'(.*?)'/).flatten
|
||||
@desc = m[3]
|
||||
@obsolete = ! m[4].nil?
|
||||
@sup = (m[5]||"").scan(SCAN_WOID).flatten
|
||||
@struct = m[6] ? m[6].downcase.intern : :structural
|
||||
@must = (m[7]||"").scan(SCAN_WOID).flatten
|
||||
@may = (m[8]||"").scan(SCAN_WOID).flatten
|
||||
@def = nil
|
||||
end
|
||||
|
||||
def name
|
||||
@names.first
|
||||
end
|
||||
|
||||
def to_s
|
||||
(@names && @names.first) || @oid
|
||||
end
|
||||
|
||||
def changed
|
||||
@def = nil
|
||||
end
|
||||
|
||||
def to_def
|
||||
return @def if @def
|
||||
ans = "( #{@oid} "
|
||||
if @names.nil? or @names.empty?
|
||||
# nothing
|
||||
elsif @names.size == 1
|
||||
ans << "NAME '#{@names.first}' "
|
||||
else
|
||||
ans << "NAME ( "
|
||||
@names.each { |n| ans << "'#{n}' " }
|
||||
ans << ") "
|
||||
end
|
||||
ans << "DESC '#{@desc}' " if @desc
|
||||
ans << "OBSOLETE " if @obsolete
|
||||
ans << joinoids("SUP ",@sup," ")
|
||||
ans << "#{@struct.to_s.upcase} " if @struct
|
||||
ans << joinoids("MUST ",@must," ")
|
||||
ans << joinoids("MAY ",@may," ")
|
||||
ans << ")"
|
||||
@def = ans
|
||||
end
|
||||
|
||||
def joinoids(pfx,arr,sfx)
|
||||
return "" unless arr and !arr.empty?
|
||||
return "#{pfx}#{arr}#{sfx}" unless arr.is_a?(Array)
|
||||
a = arr.collect { |elem| elem.to_s }
|
||||
if a.size == 1
|
||||
return "#{pfx}#{a.first}#{sfx}"
|
||||
else
|
||||
return "#{pfx}( #{a.join(" $ ")} )#{sfx}"
|
||||
end
|
||||
end
|
||||
end # class ObjectClass
|
||||
|
||||
end # class Schema
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,123 @@
|
||||
require 'ldap/server/connection'
|
||||
require 'ldap/server/operation'
|
||||
require 'openssl'
|
||||
require 'logger'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
attr_accessor :root_dse
|
||||
|
||||
DEFAULT_OPT = {
|
||||
:port=>389,
|
||||
:nodelay=>true,
|
||||
}
|
||||
|
||||
# Create a new server. Options include all those to tcpserver/preforkserver
|
||||
# plus:
|
||||
# either
|
||||
# :router=>Router - request router instance
|
||||
# or
|
||||
# :operation_class=>Class - set Operation handler class
|
||||
# :operation_args=>[...] - args to Operation.new
|
||||
#
|
||||
# :ssl_key_file=>pem, :ssl_cert_file=>pem - enable SSL
|
||||
# :ssl_ca_path=>directory - verify peer certificates
|
||||
# :schema=>Schema - Schema object
|
||||
# :namingContexts=>[dn, ...] - base DN(s) we answer
|
||||
#
|
||||
# Specifying a :router always overrides :operation_class
|
||||
|
||||
attr_reader :logger
|
||||
|
||||
def initialize(opt = DEFAULT_OPT)
|
||||
@opt = opt
|
||||
@opt[:server] = self
|
||||
if @opt[:router]
|
||||
@opt.delete(:operation_class)
|
||||
@opt.delete(:operation_args)
|
||||
else
|
||||
@opt[:operation_class] ||= LDAP::Server::Operation
|
||||
@opt[:operation_args] ||= []
|
||||
end
|
||||
unless @opt[:logger]
|
||||
@opt[:logger] ||= Logger.new($stderr)
|
||||
@opt[:logger].level = Logger::INFO
|
||||
end
|
||||
@logger = @opt[:logger]
|
||||
LDAP::Server.ssl_prepare(@opt)
|
||||
@schema = opt[:schema] # may be nil
|
||||
@root_dse = Hash.new { |h,k| h[k] = [] }.merge({
|
||||
'objectClass' => ['top','openLDAProotDSE','extensibleObject'],
|
||||
'supportedLDAPVersion' => ['3'],
|
||||
#'altServer' =>
|
||||
#'supportedExtension' =>
|
||||
#'supportedControl' =>
|
||||
#'supportedSASLMechanisms' =>
|
||||
})
|
||||
@root_dse['subschemaSubentry'] = [@schema.subschema_dn] if @schema
|
||||
@root_dse['namingContexts'] = opt[:namingContexts] if opt[:namingContexts]
|
||||
end
|
||||
|
||||
# create opt[:ssl_ctx] from the other ssl options
|
||||
|
||||
def self.ssl_prepare(opt) # :nodoc:
|
||||
if opt[:ssl_key_file] and opt[:ssl_cert_file]
|
||||
ctx = OpenSSL::SSL::SSLContext.new
|
||||
ctx.key = OpenSSL::PKey::RSA.new(File::read(opt[:ssl_key_file]))
|
||||
ctx.cert = OpenSSL::X509::Certificate.new(File::read(opt[:ssl_cert_file]))
|
||||
if opt[:ssl_dhparams]
|
||||
ctx.tmp_dh_callback = proc { |*args|
|
||||
OpenSSL::PKey::DH.new(
|
||||
File.read(opt[:ssl_dhparams])
|
||||
)
|
||||
}
|
||||
end
|
||||
if opt[:ssl_ca_path]
|
||||
ctx.ca_path = opt[:ssl_ca_path]
|
||||
ctx.verify_mode = opt[:ssl_verify_mode] ||
|
||||
OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
||||
elsif opt[:ssl_verify_mode] != 0
|
||||
$stderr.puts "Warning: No ssl_ca_path, peer certificate won't be verified"
|
||||
end
|
||||
opt[:ssl_ctx] = ctx
|
||||
end
|
||||
end
|
||||
|
||||
def run_tcpserver
|
||||
require 'ldap/server/tcpserver'
|
||||
|
||||
opt = @opt
|
||||
@thread = LDAP::Server.tcpserver(@opt) do
|
||||
LDAP::Server::Connection::new(self,opt).handle_requests
|
||||
end
|
||||
end
|
||||
|
||||
def run_prefork
|
||||
require 'ldap/server/preforkserver'
|
||||
|
||||
opt = @opt
|
||||
@thread = LDAP::Server.preforkserver(@opt) do
|
||||
LDAP::Server::Connection::new(self,opt).handle_requests
|
||||
end
|
||||
end
|
||||
|
||||
def join
|
||||
begin
|
||||
@thread.join
|
||||
rescue Interrupt
|
||||
@logger.info "Exiting..."
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
@thread.raise Interrupt, "" # <= temporary fix for 1.8.6
|
||||
begin
|
||||
@thread.join
|
||||
rescue Interrupt
|
||||
# nop
|
||||
end
|
||||
end
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,235 @@
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# A class which describes LDAP SyntaxDescriptions. For now there is
|
||||
# a global pool of Syntax objects (rather than each Schema object
|
||||
# having its own set)
|
||||
|
||||
class Syntax
|
||||
attr_reader :oid, :nhr, :binary, :desc
|
||||
|
||||
# Create a new Syntax object
|
||||
|
||||
def initialize(oid, desc=nil, opt={}, &blk)
|
||||
@oid = oid
|
||||
@desc = desc
|
||||
@nhr = opt[:nhr] # not human-readable?
|
||||
@binary = opt[:binary] # binary encoding forced?
|
||||
@re = opt[:re] # regular expression for parsing
|
||||
@def = nil
|
||||
instance_eval(&blk) if blk
|
||||
end
|
||||
|
||||
def to_s
|
||||
@oid
|
||||
end
|
||||
|
||||
# Create a new Syntax object, given its description string
|
||||
|
||||
def self.from_def(str, &blk)
|
||||
m = LDAPSyntaxDescription.match(str)
|
||||
raise LDAP::ResultError::InvalidAttributeSyntax,
|
||||
"Bad SyntaxTypeDescription #{str.inspect}" unless m
|
||||
new(m[1], m[2], :nhr=>(m[3] == 'TRUE'), :binary=>(m[4] == 'TRUE'), &blk)
|
||||
end
|
||||
|
||||
# Convert this object to its description string
|
||||
|
||||
def to_def
|
||||
return @def if @def
|
||||
ans = "( #@oid "
|
||||
ans << "DESC '#@desc' " if @desc
|
||||
# These are OpenLDAP extensions
|
||||
ans << "X-BINARY-TRANSFER-REQUIRED 'TRUE' " if @binary
|
||||
ans << "X-NOT-HUMAN-READABLE 'TRUE' " if @nhr
|
||||
ans << ")"
|
||||
@def = ans
|
||||
end
|
||||
|
||||
# Return true or a MatchData object if the given value is allowed
|
||||
# by this syntax
|
||||
|
||||
def match(val)
|
||||
return true if @re.nil?
|
||||
@re.match(value_to_s(val))
|
||||
end
|
||||
|
||||
# Convert a value for this syntax into its canonical string representation
|
||||
# (not yet used, but seemed like a good idea)
|
||||
|
||||
def value_to_s(val)
|
||||
val.to_s
|
||||
end
|
||||
|
||||
# Convert a string value for this syntax into a Ruby-like value
|
||||
# (not yet used, but seemed like a good idea)
|
||||
|
||||
def value_from_s(val)
|
||||
val
|
||||
end
|
||||
|
||||
@@syntaxes = {}
|
||||
|
||||
# Add a new syntax definition
|
||||
|
||||
def self.add(*args, &blk)
|
||||
s = new(*args, &blk)
|
||||
@@syntaxes[s.oid] = s
|
||||
end
|
||||
|
||||
# Find a Syntax object given an oid. If not known, return a new empty
|
||||
# Syntax object associated with this oid.
|
||||
|
||||
def self.find(oid)
|
||||
return oid if oid.nil? or oid.is_a?(LDAP::Server::Syntax)
|
||||
return @@syntaxes[oid] if @@syntaxes[oid]
|
||||
add(oid)
|
||||
end
|
||||
|
||||
# Return all known syntax objects
|
||||
|
||||
def self.all_syntaxes
|
||||
@@syntaxes.values.uniq
|
||||
end
|
||||
|
||||
# Shared constants for regexp-based syntax parsers
|
||||
|
||||
KEYSTR = "[a-zA-Z][a-zA-Z0-9;-]*"
|
||||
NUMERICOID = "( \\d[\\d.]+\\d )"
|
||||
WOID = "\\s* ( #{KEYSTR} | \\d[\\d.]+\\d ) \\s*"
|
||||
_WOID = "\\s* (?: #{KEYSTR} | \\d[\\d.]+\\d ) \\s*"
|
||||
OIDS = "( #{_WOID} | \\s* \\( #{_WOID} (?: \\$ #{_WOID} )* \\) \\s* )"
|
||||
_QDESCR = "\\s* ' #{KEYSTR} ' \\s*"
|
||||
QDESCRS = "( #{_QDESCR} | \\s* \\( (?:#{_QDESCR})+ \\) \\s* )"
|
||||
QDSTRING = "\\s* ' (.*?) ' \\s*"
|
||||
NOIDLEN = "(\\d[\\d.]+\\d) (?: \\{ (\\d+) \\} )?"
|
||||
ATTRIBUTEUSAGE = "(userApplications|directoryOperation|distributedOperation|dSAOperation)"
|
||||
|
||||
end
|
||||
|
||||
class Syntax
|
||||
|
||||
# These are the 'SHOULD' support syntaxes from RFC2252 section 6
|
||||
|
||||
AttributeTypeDescription =
|
||||
add("1.3.6.1.4.1.1466.115.121.1.3", "Attribute Type Description", :re=>
|
||||
%r! \A \s* \( \s*
|
||||
#{NUMERICOID} \s*
|
||||
(?: NAME #{QDESCRS} )?
|
||||
(?: DESC #{QDSTRING} )?
|
||||
( OBSOLETE \s* )?
|
||||
(?: SUP #{WOID} )?
|
||||
(?: EQUALITY #{WOID} )?
|
||||
(?: ORDERING #{WOID} )?
|
||||
(?: SUBSTR #{WOID} )?
|
||||
(?: SYNTAX \s* #{NOIDLEN} \s* )? # capture 2
|
||||
( SINGLE-VALUE \s* )?
|
||||
( COLLECTIVE \s* )?
|
||||
( NO-USER-MODIFICATION \s* )?
|
||||
(?: USAGE \s* #{ATTRIBUTEUSAGE} )?
|
||||
\s* \) \s* \z !xu)
|
||||
|
||||
add("1.3.6.1.4.1.1466.115.121.1.5", "Binary", :nhr=>true)
|
||||
# FIXME: value_to_s should BER-encode the value??
|
||||
|
||||
add("1.3.6.1.4.1.1466.115.121.1.6", "Bit String", :re=>/\A'([01]*)'B\z/)
|
||||
# FIXME: convert to FixNum?
|
||||
|
||||
add("1.3.6.1.4.1.1466.115.121.1.7", "Boolean", :re=>/\A(TRUE|FALSE)\z/) do
|
||||
def self.value_to_s(v)
|
||||
return v if v.is_a?(string)
|
||||
v ? "TRUE" : "FALSE"
|
||||
end
|
||||
def self.value_from_s(v)
|
||||
v.upcase == "TRUE"
|
||||
end
|
||||
end
|
||||
|
||||
add("1.3.6.1.4.1.1466.115.121.1.8", "Certificate", :binary=>true, :nhr=>true)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.9", "Certificate List", :binary=>true, :nhr=>true)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.10", "Certificate Pair", :binary=>true, :nhr=>true)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.11", "Country String", :re=>/\A[A-Z]{2}\z/i)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.12", "Distinguished Name")
|
||||
# FIXME: validate DN?
|
||||
add("1.3.6.1.4.1.1466.115.121.1.15", "Directory String")
|
||||
# missed due to lack of interest: "DIT Content Rule Description"
|
||||
add("1.3.6.1.4.1.1466.115.121.1.22", "Facsimile Telephone Number")
|
||||
add(" 1.3.6.1.4.1.1466.115.121.1.23", "Fax", :nhr=>true)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.24", "Generalized Time")
|
||||
# FIXME: Validate Generalized Time (find X.208) and convert to/from Ruby Time
|
||||
add("1.3.6.1.4.1.1466.115.121.1.26", "IA5 String")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.27", "Integer", :re=>/\A\d+\z/) do
|
||||
def self.value_from_s(v)
|
||||
v.to_i
|
||||
end
|
||||
end
|
||||
add("1.3.6.1.4.1.1466.115.121.1.28", "JPEG", :nhr=>true)
|
||||
MatchingRuleDescription =
|
||||
add("1.3.6.1.4.1.1466.115.121.1.30", "Matching Rule Description", :re=>
|
||||
%r! \A \s* \( \s*
|
||||
#{NUMERICOID} \s*
|
||||
(?: NAME #{QDESCRS} )?
|
||||
(?: DESC #{QDSTRING} )?
|
||||
( OBSOLETE \s* )?
|
||||
SYNTAX \s* #{NUMERICOID} \s*
|
||||
\s* \) \s* \z !xu)
|
||||
MatchingRuleUseDescription =
|
||||
add("1.3.6.1.4.1.1466.115.121.1.31", "Matching Rule Use Description", :re=>
|
||||
%r! \A \s* \( \s*
|
||||
#{NUMERICOID} \s*
|
||||
(?: NAME #{QDESCRS} )?
|
||||
(?: DESC #{QDSTRING} )?
|
||||
( OBSOLETE \s* )?
|
||||
APPLIES \s* #{OIDS} \s*
|
||||
\s* \) \s* \z !xu)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.33", "MHS OR Address")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.34", "Name And Optional UID")
|
||||
# missed due to lack of interest: "Name Form Description"
|
||||
add("1.3.6.1.4.1.1466.115.121.1.36", "Numeric String", :re=>/\A\d+\z/)
|
||||
ObjectClassDescription =
|
||||
add("1.3.6.1.4.1.1466.115.121.1.37", "Object Class Description", :re=>
|
||||
%r! \A \s* \( \s*
|
||||
#{NUMERICOID} \s*
|
||||
(?: NAME #{QDESCRS} )?
|
||||
(?: DESC #{QDSTRING} )?
|
||||
( OBSOLETE \s* )?
|
||||
(?: SUP #{OIDS} )?
|
||||
(?: ( ABSTRACT|STRUCTURAL|AUXILIARY ) \s* )?
|
||||
(?: MUST #{OIDS} )?
|
||||
(?: MAY #{OIDS} )?
|
||||
\s* \) \s* \z !xu)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.38", "OID", :re=>/\A#{WOID}\z/xu)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.39", "Other Mailbox")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.41", "Postal Address") do
|
||||
def self.value_from_s(v)
|
||||
v.split(/\$/)
|
||||
end
|
||||
def self.value_to_s(v)
|
||||
return v.join("$") if v.is_a?(Array)
|
||||
return v
|
||||
end
|
||||
end
|
||||
add("1.3.6.1.4.1.1466.115.121.1.43", "Presentation Address")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.44", "Printable String")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.50", "Telephone Number")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.53", "UTC Time")
|
||||
|
||||
LDAPSyntaxDescription =
|
||||
add("1.3.6.1.4.1.1466.115.121.1.54", "LDAP Syntax Description", :re=>
|
||||
%r! \A \s* \( \s*
|
||||
#{NUMERICOID} \s*
|
||||
(?: DESC #{QDSTRING} )?
|
||||
(?: X-BINARY-TRANSFER-REQUIRED \s* ' (TRUE|FALSE) ' \s* )?
|
||||
(?: X-NOT-HUMAN-READABLE \s* ' (TRUE|FALSE) ' \s* )?
|
||||
\s* \) \s* \z !xu)
|
||||
|
||||
# Missed due to lack of interest: "DIT Structure Rule Description"
|
||||
|
||||
# A few others from RFC2252 section 4.3.2
|
||||
add("1.3.6.1.4.1.1466.115.121.1.4", "Audio", :nhr=>true)
|
||||
add("1.3.6.1.4.1.1466.115.121.1.40", "Octet String")
|
||||
add("1.3.6.1.4.1.1466.115.121.1.58", "Substring Assertion")
|
||||
end
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,102 @@
|
||||
require 'socket'
|
||||
require 'fileutils'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
# Accept connections on a port, and for each one start a new thread
|
||||
# and run the given block. Returns the Thread object for the listener.
|
||||
#
|
||||
# FIXME:
|
||||
# - have a limit on total number of concurrent connects
|
||||
# - have a limit on connections from a single IP, or from a /24
|
||||
# (to avoid the trivial DoS that the first limit creates)
|
||||
# - ACL using source IP address (or perhaps that belongs in application)
|
||||
#
|
||||
# Options:
|
||||
# :port=>port number [required]
|
||||
# :bindaddr=>"IP address"
|
||||
# :user=>"username" - drop privileges after bind
|
||||
# :group=>"groupname" - ditto
|
||||
# :logger=>object - implements << method
|
||||
# :listen=>number - listen queue depth
|
||||
# :nodelay=>true - set TCP_NODELAY option
|
||||
|
||||
def self.tcpserver(opt, &blk)
|
||||
if opt[:socket]
|
||||
server = UNIXServer.new(opt[:socket])
|
||||
FileUtils.chmod(0777, opt[:socket])
|
||||
else
|
||||
server = TCPServer.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
|
||||
end
|
||||
|
||||
# Drop privileges if requested
|
||||
require 'etc' if opt[:group] or opt[:user]
|
||||
Process.gid = Process.egid = Etc.getgrnam(opt[:group]).gid if opt[:group]
|
||||
Process.uid = Process.euid = Etc.getpwnam(opt[:user]).uid if opt[:user]
|
||||
|
||||
Process.gid = opt[:gid] if opt[:gid]
|
||||
Process.uid = opt[:uid] if opt[:uid]
|
||||
|
||||
if opt[:socket]
|
||||
FileUtils.chown((opt[:user] || opt[:uid]), (opt[:group] || opt[:gid]), opt[:socket])
|
||||
end
|
||||
|
||||
# Typically the O/S will buffer response data for 100ms before sending.
|
||||
# If the response is sent as a single write() then there's no need for it.
|
||||
if opt[:nodelay]
|
||||
begin
|
||||
server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
||||
rescue Exception
|
||||
end
|
||||
end
|
||||
# set queue size for incoming connections (default is 5)
|
||||
server.listen(opt[:listen]) if opt[:listen]
|
||||
|
||||
Thread.new do
|
||||
while true
|
||||
begin
|
||||
session = server.accept
|
||||
# subtlety: copy 'session' into a block-local variable because
|
||||
# it will change when the next session is accepted
|
||||
Thread.new(session) do |s|
|
||||
begin
|
||||
s.instance_eval(&blk)
|
||||
rescue Exception => e
|
||||
opt[:logger].error(s.peeraddr[3]) {"#{e}: #{e.backtrace[0]}"}
|
||||
ensure
|
||||
s.close
|
||||
end
|
||||
end
|
||||
rescue Interrupt
|
||||
# This exception can be raised to shut the server down
|
||||
server.close if server and not server.closed?
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
|
||||
if __FILE__ == $0
|
||||
# simple test
|
||||
puts "Running a test POP3 server on port 1110"
|
||||
t = LDAP::Server.tcpserver(:port=>1110) do
|
||||
print "+OK I am a fake POP3 server\r\n"
|
||||
while line = gets
|
||||
case line
|
||||
when /^quit/i
|
||||
break
|
||||
when /^crash/i
|
||||
raise Errno::EPERM, "dammit!"
|
||||
else
|
||||
print "-ERR I don't understand #{line}"
|
||||
end
|
||||
end
|
||||
print "+OK bye\r\n"
|
||||
end
|
||||
#sleep 10; t.raise Interrupt # uncomment to run for fixed time period
|
||||
t.join
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
require 'ldap/server/dn'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
class Trie
|
||||
|
||||
# Trie or prefix tree suitable for storing LDAP paths
|
||||
# Variables (wildcards) are supported
|
||||
|
||||
class NodeNotFoundError < Error; end
|
||||
|
||||
attr_accessor :parent, :value, :children
|
||||
|
||||
# Create a new Trie. Use with a block
|
||||
def initialize(parent = nil, value = nil)
|
||||
@parent = parent
|
||||
@value = value
|
||||
@children = Hash.new
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
# Insert a path (empty node)
|
||||
def <<(dn)
|
||||
insert(dn)
|
||||
end
|
||||
|
||||
# Insert a node with a value
|
||||
def insert(dn, value = nil)
|
||||
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
|
||||
dn.reverse_each do |component|
|
||||
@children[component] = Trie.new(self) if @children[component].nil?
|
||||
dn.dname.pop
|
||||
if dn.any?
|
||||
@children[component].insert dn, value
|
||||
else
|
||||
@children[component].value = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Looks up a node and returns its value or raises
|
||||
# LDAP::Server::Trie::NodeNotFoundError if it's not in the tree
|
||||
def lookup(dn)
|
||||
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
|
||||
return @value if dn.dname.empty?
|
||||
component = dn.dname.pop
|
||||
@children.each do |key, value|
|
||||
if key.keys.first == component.keys.first
|
||||
if key.values.first.start_with?(':') or key.values.first == component.values.first
|
||||
return value.lookup dn
|
||||
end
|
||||
end
|
||||
end
|
||||
raise NodeNotFoundError
|
||||
end
|
||||
|
||||
# Looks up a node and returns its value or the (non-nil) value of
|
||||
# the nearest ancestor.
|
||||
def match(dn, path = '')
|
||||
dn = LDAP::Server::DN.new(dn || '') if not dn.is_a? LDAP::Server::DN
|
||||
return path, @value if dn.dname.empty?
|
||||
component = dn.dname.pop
|
||||
@children.each do |key, value|
|
||||
if key.keys.first == component.keys.first
|
||||
if key.values.first.start_with?(':') or key.values.first == component.values.first
|
||||
path.prepend ',' unless path.empty?
|
||||
path.prepend "#{LDAP::Server::DN.join key}"
|
||||
new_path, new_value = value.match dn, path
|
||||
if new_value
|
||||
return new_path, new_value
|
||||
else
|
||||
return (@value ? path : nil), @value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return path, @value
|
||||
end
|
||||
|
||||
def print_tree(prefix = '')
|
||||
if @value
|
||||
p "#{prefix}{{#{@value}}}"
|
||||
end
|
||||
@children.each do |key, value|
|
||||
p "#{prefix}#{key.keys.first} => #{key.values.first}"
|
||||
@children[key].print_tree("#{prefix} ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
require 'ldap/server/result'
|
||||
|
||||
module LDAP
|
||||
class Server
|
||||
|
||||
class Operation
|
||||
|
||||
# Return true if connection is not authenticated
|
||||
|
||||
def anonymous?
|
||||
@connection.binddn.nil?
|
||||
end
|
||||
|
||||
# Split dn string into its component parts, returning
|
||||
# [ {attr=>val}, {attr=>val}, ... ]
|
||||
#
|
||||
# This is pretty horrible legacy stuff from X500; see RFC2253 for the
|
||||
# full gore. It's stupid that the LDAP protocol sends the DN in string
|
||||
# form, rather than in ASN1 form (as it does with search filters, for
|
||||
# example), even though the DN syntax is defined in terms of ASN1!
|
||||
#
|
||||
# Attribute names are downcased, but values are not. For any
|
||||
# case-insensitive attributes it's up to you to downcase them.
|
||||
#
|
||||
# Note that only v2 clients should add extra space around the comma.
|
||||
# This is accepted, and so is semicolon instead of comma, but the
|
||||
# full RFC1779 backwards-compatibility rules (e.g. quoted values)
|
||||
# are not implemented.
|
||||
#
|
||||
# I *think* these functions will work correctly with UTF8-encoded
|
||||
# characters, given that a multibyte UTF8 character does not contain
|
||||
# the bytes 00-7F and therefore we cannot confuse '\', '+' etc
|
||||
|
||||
def self.split_dn(dn)
|
||||
# convert \\ to \5c, \+ to \2b etc
|
||||
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format "\\%02x", match[1].ord }
|
||||
|
||||
# Now we know that \\ and \, do not exist, it's safe to split
|
||||
parts = dn.split(/\s*[,;]\s*/)
|
||||
|
||||
parts.collect do |part|
|
||||
res = {}
|
||||
|
||||
# Split each part into attr=val+attr=val
|
||||
avs = part.split(/\+/)
|
||||
|
||||
avs.each do |av|
|
||||
# These should all be of form attr=value
|
||||
unless av =~ /^([^=]+)=(.*)$/
|
||||
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}"
|
||||
end
|
||||
attr, val = $1.downcase, $2
|
||||
# Now we can decode those bits
|
||||
attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
|
||||
val.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
|
||||
res[attr] = val
|
||||
end
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
# Reverse of split_dn. Join [elements...]
|
||||
# where each element can be {attr=>val,...} or [[attr,val],...]
|
||||
# or just [attr,val]
|
||||
|
||||
def self.join_dn(elements)
|
||||
dn = ""
|
||||
elements.each do |elem|
|
||||
av = ""
|
||||
elem = [elem] if elem[0].is_a?(String)
|
||||
elem.each do |attr,val|
|
||||
av << "+" unless av == ""
|
||||
|
||||
av << attr << "=" <<
|
||||
val.sub(/^([# ])/, '\\\\\\1').
|
||||
sub(/( )$/, '\\\\\\1').
|
||||
gsub(/([,+"\\<>;])/, '\\\\\\1')
|
||||
end
|
||||
dn << "," unless dn == ""
|
||||
dn << av
|
||||
end
|
||||
dn
|
||||
end
|
||||
|
||||
end # class Operation
|
||||
|
||||
end # class Server
|
||||
end # module LDAP
|
||||
@@ -0,0 +1,5 @@
|
||||
module LDAP #:nodoc:
|
||||
class Server #:nodoc:
|
||||
VERSION = '0.7.0'
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user