# -*- ruby encoding: utf-8 -*- require 'ostruct' ## # Defines the Protocol Data Unit (PDU) for LDAP. An LDAP PDU always looks # like a BER SEQUENCE with at least two elements: an INTEGER message ID # number and an application-specific SEQUENCE. Some LDAPv3 packets also # include an optional third element, a sequence of "controls" (see RFC 2251 # section 4.1.12 for more information). # # The application-specific tag in the sequence tells us what kind of packet # it is, and each kind has its own format, defined in RFC-1777. # # Observe that many clients (such as ldapsearch) do not necessarily enforce # the expected application tags on received protocol packets. This # implementation does interpret the RFC strictly in this regard, and it # remains to be seen whether there are servers out there that will not work # well with our approach. # # Currently, we only support controls on SearchResult. class Net::LDAP::PDU class Error < RuntimeError; end ## # This message packet is a bind request. BindRequest = 0 BindResult = 1 UnbindRequest = 2 SearchRequest = 3 SearchReturnedData = 4 SearchResult = 5 ModifyResponse = 7 AddResponse = 9 DeleteResponse = 11 ModifyRDNResponse = 13 SearchResultReferral = 19 ExtendedRequest = 23 ExtendedResponse = 24 ## # The LDAP packet message ID. attr_reader :message_id alias_method :msg_id, :message_id ## # The application protocol format tag. attr_reader :app_tag attr_reader :search_entry attr_reader :search_referrals attr_reader :search_parameters attr_reader :bind_parameters ## # Returns RFC-2251 Controls if any. attr_reader :ldap_controls alias_method :result_controls, :ldap_controls # Messy. Does this functionality belong somewhere else? def initialize(ber_object) begin @message_id = ber_object[0].to_i # Grab the bottom five bits of the identifier so we know which type of # PDU this is. # # This is safe enough in LDAP-land, but it is recommended that other # approaches be taken for other protocols in the case that there's an # app-specific tag that has both primitive and constructed forms. @app_tag = ber_object[1].ber_identifier & 0x1f @ldap_controls = [] rescue Exception => ex raise Net::LDAP::PDU::Error, "LDAP PDU Format Error: #{ex.message}" end case @app_tag when BindResult parse_bind_response(ber_object[1]) when SearchReturnedData parse_search_return(ber_object[1]) when SearchResultReferral parse_search_referral(ber_object[1]) when SearchResult parse_ldap_result(ber_object[1]) when ModifyResponse parse_ldap_result(ber_object[1]) when AddResponse parse_ldap_result(ber_object[1]) when DeleteResponse parse_ldap_result(ber_object[1]) when ModifyRDNResponse parse_ldap_result(ber_object[1]) when SearchRequest parse_ldap_search_request(ber_object[1]) when BindRequest parse_bind_request(ber_object[1]) when UnbindRequest parse_unbind_request(ber_object[1]) when ExtendedResponse parse_ldap_result(ber_object[1]) else raise LdapPduError.new("unknown pdu-type: #{@app_tag}") end parse_controls(ber_object[2]) if ber_object[2] end ## # Returns a hash which (usually) defines the members :resultCode, # :errorMessage, and :matchedDN. These values come directly from an LDAP # response packet returned by the remote peer. Also see #result_code. def result @ldap_result || {} end def error_message result[:errorMessage] || "" end ## # This returns an LDAP result code taken from the PDU, but it will be nil # if there wasn't a result code. That can easily happen depending on the # type of packet. def result_code(code = :resultCode) @ldap_result and @ldap_result[code] end def status result_code == 0 ? :success : :failure end def success? status == :success end def failure? !success? end ## # Return serverSaslCreds, which are only present in BindResponse packets. #-- # Messy. Does this functionality belong somewhere else? We ought to # refactor the accessors of this class before they get any kludgier. def result_server_sasl_creds @ldap_result && @ldap_result[:serverSaslCreds] end def parse_ldap_result(sequence) sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length." @ldap_result = { :resultCode => sequence[0], :matchedDN => sequence[1], :errorMessage => sequence[2] } parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == 10 end private :parse_ldap_result ## # A Bind Response may have an additional field, ID [7], serverSaslCreds, # per RFC 2251 pgh 4.2.3. def parse_bind_response(sequence) sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP Bind Response length." parse_ldap_result(sequence) @ldap_result[:serverSaslCreds] = sequence[3] if sequence.length >= 4 @ldap_result end private :parse_bind_response # Definition from RFC 1777 (we're handling application-4 here). # # Search Response ::= # CHOICE { # entry [APPLICATION 4] SEQUENCE { # objectName LDAPDN, # attributes SEQUENCE OF SEQUENCE { # AttributeType, # SET OF AttributeValue # } # }, # resultCode [APPLICATION 5] LDAPResult # } # # We concoct a search response that is a hash of the returned attribute # values. # # NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES. # # This is to make them more predictable for user programs, but it may not # be a good idea. Maybe this should be configurable. def parse_search_return(sequence) sequence.length >= 2 or raise Net::LDAP::PDU::Error, "Invalid Search Response length." @search_entry = Net::LDAP::Entry.new(sequence[0]) sequence[1].each { |seq| @search_entry[seq[0]] = seq[1] } end private :parse_search_return ## # A search referral is a sequence of one or more LDAP URIs. Any number of # search-referral replies can be returned by the server, interspersed with # normal replies in any order. #-- # Until I can think of a better way to do this, we'll return the referrals # as an array. It'll be up to higher-level handlers to expose something # reasonable to the client. def parse_search_referral(uris) @search_referrals = uris end private :parse_search_referral ## # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL # Octet String. If only two fields are given, the second one may be either # criticality or data, since criticality has a default value. Someday we # may want to come back here and add support for some of more-widely used # controls. RFC-2696 is a good example. def parse_controls(sequence) @ldap_controls = sequence.map do |control| o = OpenStruct.new o.oid, o.criticality, o.value = control[0], control[1], control[2] if o.criticality and o.criticality.is_a?(String) o.value = o.criticality o.criticality = false end o end end private :parse_controls # (provisional, must document) def parse_ldap_search_request(sequence) s = OpenStruct.new s.base_object, s.scope, s.deref_aliases, s.size_limit, s.time_limit, s.types_only, s.filter, s.attributes = sequence @search_parameters = s end private :parse_ldap_search_request # (provisional, must document) def parse_bind_request sequence s = OpenStruct.new s.version, s.name, s.authentication = sequence @bind_parameters = s end private :parse_bind_request # (provisional, must document) # UnbindRequest has no content so this is a no-op. def parse_unbind_request(sequence) nil end private :parse_unbind_request end module Net ## # Handle renamed constants Net::LdapPdu (Net::LDAP::PDU) and # Net::LdapPduError (Net::LDAP::PDU::Error). def self.const_missing(name) #:nodoc: case name.to_s when "LdapPdu" warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU instead." Net::LDAP::PDU when "LdapPduError" warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU::Error instead." Net::LDAP::PDU::Error when 'LDAP' else super end end end # module Net