# -*- coding: binary -*-
module Rex
	module Parser

		# Determines if Nokogiri is available and if it's a minimum
		# acceptable version.
		def self.load_nokogiri
			@nokogiri_loaded = false
			begin
				require 'nokogiri'
				major,minor = Nokogiri::VERSION.split(".")[0,2]
				if major.to_i >= 1
					if minor.to_i >= 4
						@nokogiri_loaded = true
					end
				end
			rescue LoadError => e
				@nokogiri_loaded = false
				@nokogiri_error  = e
			end
			@nokogiri_loaded
		end

		def self.nokogiri_loaded
			!!@nokogiri_loaded
		end

		# Useful during development, shouldn't be used in normal operation.
		def self.reload(fname)
			$stdout.puts "Reloading #{fname}..."
			load __FILE__
			load File.join(File.expand_path(File.dirname(__FILE__)),fname)
		end

	end
end

module Rex
module Parser

		load_nokogiri && module NokogiriDocMixin

		# Set up the getters and instance variables for the document
		eval("attr_reader :args, :db, :state, :block, :report_data")

		def initialize(args,db,&block)
			@args = args
			@db = db
			@state = {}
			@state[:current_tag] = {}
			@block = block if block
			@report_data = {:wspace => args[:wspace]}
			@nx_console_id = args[:nx_console_id]
			super()
		end

		# Turn XML attribute pairs in to more workable hashes (there
		# are better Enumerable tricks in Ruby 1.9, but ignoring for now)
		def attr_hash(attrs)
			h = {}
			attrs.each {|k,v| h[k] = v}
			h
		end

		def valid_ip(addr)
			valid = false
			valid = ::Rex::Socket::RangeWalker.new(addr).valid? rescue false
			!!valid
		end

		def normalize_ref(ref_type, ref_value)
			return if ref_type.nil? || ref_type.empty? || ref_value.nil? || ref_value.empty?
			ref_value = ref_value.strip
			ref_type = ref_type.strip.upcase

			ret = case ref_type
				when "CVE"
					ref_value.gsub("CAN", "CVE")
				when "MS"
					if ref_value =~ /^MS[0-9]/
						"MSB-#{ref_value}"
					else
						"MSB-MS#{ref_value}"
					end
				when "URL", "BID"
					"#{ref_type}-#{ref_value}"
				when "APPLE"
					ref_value
				when "XF"
					if ref_value =~ /\((\d+)\)$/
						"#{ref_type}-#{$1}"
					else
						"#{ref_type}-#{ref_value}"
					end
				else # Handle others?
					"#{ref_type}-#{ref_value}"
				end
			return ret
		end

		def normalize_references(orig_refs)
			return [] unless orig_refs
			refs = []
			orig_refs.each do |ref_hash|

				ref_hash_sym = Hash[ref_hash.map {|k, v| [k.to_sym, v] }]
				ref_type = ref_hash_sym[:source].to_s.strip.upcase
				ref_value = ref_hash_sym[:value].to_s.strip
				refs << normalize_ref(ref_type, ref_value)
			end
			return refs.compact.uniq
		end

		def in_tag(tagname)
			@state[:current_tag].keys.include? tagname
		end

		# If there's an address, it's not on the blacklist,
		# it has ports, and the port list isn't
		# empty... it's okay.
		def host_is_okay
			return false unless @report_data[:host]
			return false unless valid_ip(@report_data[:host])
			return false unless @report_data[:state] == Msf::HostState::Alive
			if @args[:blacklist]
				return false if @args[:blacklist].include?(@report_data[:host])
			end
			return false unless @report_data[:ports]
			return false if @report_data[:ports].empty?
			return true
		end

		# XXX: Document classes ought to define this
		def determine_port_state(v)
			return v
		end

		# Circumvent the unknown attribute logging by the various reporters. They
		# seem to be there just for debugging anyway.
		def db_report(table, data)
			raise "Data should be a hash" unless data.kind_of? Hash
			nonempty_data = data.reject {|k,v| v.nil?}
			valid_attrs = db_valid_attributes(table)
			raise "Unknown table `#{table}'" if valid_attrs.empty?
			case table
			when :note, :web_site, :web_page, :web_form, :web_vuln
				just_the_facts = nonempty_data
			else
				just_the_facts = nonempty_data.select {|k,v| valid_attrs.include? k.to_s.to_sym}
      end
      return nil if just_the_facts.empty?
      just_the_facts[:task] = @args[:task]
			db.send("report_#{table}", just_the_facts)
		end

		# XXX: It would be better to either have a single registry of acceptable
		# keys if we're going to alert on bad ones, or to be more forgiving if
		# the caller is this thing. There is basically no way to tell if
		# report_host()'s tastes are going to change with this scheme.
		def db_valid_attributes(table)
			case table.to_s.to_sym
			when :host
				::Mdm::Host.new.attribute_names.map {|x| x.to_sym} |
					[:host, :workspace]
			when :service
				::Mdm::Service.new.attribute_names.map {|x| x.to_sym} |
					[:host, :host_name, :mac, :workspace]
			when :vuln
				::Mdm::Vuln.new.attribute_names.map {|x| x.to_sym} |
					[:host, :refs, :workspace, :port, :proto, :details, :exploited_at]
			when :vuln_details
				::Mdm::VulnDetails.new.attribute_names.map {|x| x.to_sym} | [ :key ]
			when :host_details
				::Mdm::HostDetails.new.attribute_names.map {|x| x.to_sym} | [ :key ]
			when :note, :web_site, :web_page, :web_form, :web_vuln
				# These guys don't complain
				[:anything]
			else
				[]
			end
		end

		# Nokogiri 1.4.4 (and presumably beyond) generates attrs as pairs,
		# like [["value1","foo"],["value2","bar"]] (but not hashes for some
		# reason). 1.4.3.1 (and presumably 1.4.3.x and prior) generates attrs
		# as a flat array of strings. We want array_pairs.
		def normalize_attrs(attrs)
			attr_pairs = []
			case attrs.first
			when Array, NilClass
				attr_pairs = attrs
			when String
				attrs.each_index {|i|
					next if i % 2 == 0
					attr_pairs << [attrs[i-1],attrs[i]]
				}
			else # Wow, yet another format! It's either from the distant past or distant future.
				raise ::Msf::DBImportError.new("Unknown format for XML attributes. Please check your Nokogiri version.")
			end
			return attr_pairs
		end

		# This breaks xml-encoded characters, so need to append.
		# It's on the end_element tag name to turn the appending
		# off and clear out the data.
		def characters(text)
			return unless @state[:has_text]
			@text ||= ""
			@text << text
		end

		# Effectively the same as characters()
		def cdata_block(text)
			return unless @state[:has_text]
			@text ||= ""
			@text << text
		end

		def end_document
			block = @block
			return unless @report_type_ok
			unless @state[:current_tag].empty?
				missing_ends = @state[:current_tag].keys.map {|x| "'#{x}'"}.join(", ")
	l			msg = "Warning, the provided file is incomplete, and there may be missing\n"
				msg << "data. The following tags were not closed: #{missing_ends}."
				db.emit(:warning,msg,&block) if block
			end
		end

	end

end
end
