You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
528 lines
17 KiB
528 lines
17 KiB
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
|