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.
257 lines
7.9 KiB
257 lines
7.9 KiB
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
|