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
      @active_reqs = {}   # map message ID to thread object
      @binddn = nil
      @version = 3
      @logger = @opt[:logger] || $stderr
      @ssl = false

      startssl if @opt[:ssl_on_connect]
    end

    def log(msg)
      @logger << "[#{@io.peeraddr[3]}]: #{msg}\n"
    end

    def startssl # :yields:
      @mutex.synchronize do
        raise LDAP::ResultError::OperationsError if @ssl or @active_reqs.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
      operationClass = @opt[:operation_class]
      ocArgs = @opt[:operation_args] || []
      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
              @binddn, @version = operationClass.new(self,messageId,*ocArgs).
                                  do_bind(protocolOp, controls)

            when 2 # UnbindRequest
              throw(:close)

            when 3 # SearchRequest
              # 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 id in @active_reqs.
              # However, to avoid a race we copy messageId/
              # protocolOp/controls into thread-local variables, because
              # they will change when the next request comes in.
              #
              # There is a theoretical race condition here: a client could
              # send an abandon request before Thread.current is assigned to
              # @active_reqs[thrm]. It's not a problem, because abandon isn't
              # guaranteed to work anyway. Doing it this way ensures that
              # @active_reqs does not leak memory on a long-lived connection.

              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_search(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            when 6 # ModifyRequest
              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_modify(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            when 8 # AddRequest
              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_add(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            when 10 # DelRequest
              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_del(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            when 12 # ModifyDNRequest
              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_modifydn(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            when 14 # CompareRequest
              Thread.new(messageId,protocolOp,controls) do |thrm,thrp,thrc|
                begin
                  @active_reqs[thrm] = Thread.current
                  operationClass.new(self,thrm,*ocArgs).do_compare(thrp, thrc)
                ensure
                  @active_reqs.delete(thrm)
                end
              end

            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

    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 = @active_reqs.delete(messageID)
        thread.raise LDAP::Abandon if thread and thread.alive?
      end
    end

    def abandon_all
      return if @active_reqs.size == 0
      @mutex.synchronize do
        @active_reqs.each do |id, thread|
          thread.raise LDAP::Abandon if thread.alive?
        end
        @active_reqs = {}
      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
