# -*- coding: binary -*-
require "rex/parser/nokogiri_doc_mixin"
require 'rex'
require 'uri'

module Rex
	module Parser

		# If Nokogiri is available, define the Acunetix document class.
		load_nokogiri && class AcunetixDocument < Nokogiri::XML::SAX::Document

		include NokogiriDocMixin

		# The resolver prefers your local /etc/hosts (or windows equiv), but will
		# fall back to regular DNS. It retains a cache for the import to avoid
		# spamming your network with DNS requests.
		attr_reader :resolv_cache

		# If name resolution of the host fails out completely, you will not be
		# able to import that Scan task. Other scan tasks in the same report
		# should be unaffected.
		attr_reader :parse_warnings

		def start_document
			@parse_warnings = []
			@resolv_cache = {}
		end

		def start_element(name=nil,attrs=[])
			attrs = normalize_attrs(attrs)
			block = @block
			@state[:current_tag][name] = true
			case name
			when "Scan" # Start of the thing.
			when "Name", "StartURL", "Banner", "Os"
				@state[:has_text] = true
			when "LoginSequence" # Skipping for now
			when "Crawler"
				record_crawler(attrs)
			when "FullURL"
				@state[:has_text] = true
			when "Variable"
				record_variable(attrs)
			when "Request", "Response"
				@state[:has_text] = true
			end
		end

		def end_element(name=nil)
			block = @block
			case name
			when "Scan"
				# Clears most of the @state out, we're done with this web site.
				@state.delete_if {|k| k != :current_tag}
			when "Name"
				@state[:has_text] = false
				collect_scan_name
				collect_report_item_name
				@text = nil
			when "StartURL" # Populates @state[:starturl_uri], we use this a lot
				@state[:has_text] = false
				collect_host
				collect_service
				@text = nil
				handle_parse_warnings &block
				host_object = report_host &block
				if host_object
					report_starturl_service(host_object,&block)
					db.report_import_note(@args[:wspace],host_object)
				end
			when "Banner"
				@state[:has_text] = false
				collect_and_report_banner
			when "Os"
				@state[:has_text] = false
				report_os_fingerprint
			when "LoginSequence" # This comes up later in the report anyway
			when "Crawler"
				report_starturl_web_site(&block)
			when "FullURL"
				@state[:has_text] = false
				report_web_site(@text,&block)
				@text = nil
			when "Inputs"
				report_web_form(&block)
			when "Request"
				@state[:has_text] = false
				collect_page_request
				@text = nil
			when "Response"
				@state[:has_text] = false
				collect_page_response
				@text = nil
				report_web_page(&block)
			end
			@state[:current_tag].delete name
		end

		def collect_page_response
			return unless in_tag("TechnicalDetails")
			return unless in_tag("ReportItem")
			return unless @text
			return if @text.to_s.empty?
			@state[:page_response] = @text
		end

		def collect_page_request
			return unless in_tag("TechnicalDetails")
			return unless in_tag("ReportItem")
			return unless @text
			return if @text.to_s.empty?
			@state[:page_request] = @text
		end

		def collect_scan_name
			return unless in_tag("Scan")
			return if in_tag("ReportItems")
			return if in_tag("Crawler")
			return unless @text
			return if @text.strip.empty?
			@state[:scan_name] = @text.strip
		end

		def collect_host
			return unless in_tag("Scan")
			return unless @text
			return if @text.strip.empty?
			uri = URI.parse(@text) rescue nil
			return unless uri
			address = resolve_scan_starturl_address(uri)
			@report_data[:host] = address
			@report_data[:state] = Msf::HostState::Alive
		end

		def collect_service
			return unless @report_data[:host]
			return unless in_tag("Scan")
			return unless @text
			return if @text.strip.empty?
			uri = URI.parse(@text) rescue nil
			return unless uri
			@state[:starturl_uri] = uri
			@report_data[:ports] ||= []
			@report_data[:ports] << @state[:starturl_port]
		end

		def collect_and_report_banner
			return unless (svc = @state[:starturl_service_object]) # Yes i want assignment
			return unless @text
			return if @text.strip.empty?
			return unless in_tag("Scan")
			svc_info = {
				:host => svc.host,
				:port => svc.port,
				:proto => svc.proto,
				:info => @text.strip
			}
			db_report(:service, svc_info)
			@text = nil
		end

		def collect_report_item_name
			return unless in_tag("ReportItem")
			return unless @text
			return if @text.strip.empty?
			@state[:report_item] = @text
		end

		# @state[:fullurl] is set by report_web_site
		def record_variable(attrs)
			return unless in_tag("Inputs")
			return unless @state[:fullurl].kind_of? URI
			method = attr_hash(attrs)["Type"]
			return unless method
			return if method.strip.empty?
			@state[:form_variables] ||= []
			@state[:form_variables] << [attr_hash(attrs)["Name"],method]
		end

		def record_crawler(attrs)
			return unless in_tag("Scan")
			return unless @state[:starturl_service_object]
			starturl = attr_hash(attrs)["StartUrl"]
			return unless starturl
			@state[:crawler_starturl] = starturl
		end

		def report_web_form(&block)
			return unless in_tag("SiteFiles")
			return unless @state[:web_site]
			return unless @state[:fullurl].kind_of? URI
			return unless @state[:form_variables].kind_of? Array
			return if @state[:form_variables].empty?
			method = parse_method(@state[:form_variables].first[1])
			vars = @state[:form_variables].map {|x| x[0]}
			form_info = {}
			form_info[:web_site] = @state[:web_site]
			form_info[:path] = @state[:fullurl].path
			form_info[:query] = @state[:fullurl].query
			form_info[:method] = method
			form_info[:params] = vars
			url = @state[:fullurl].to_s
			db.emit(:web_form,url,&block) if block
			db_report(:web_form,form_info)
			@state[:fullurl] = nil
			@state[:form_variables] = nil
		end

		def report_web_page(&block)
			return if should_skip_this_page
			return unless @state[:web_site]
			return unless @state[:page_request]
			return if @state[:page_request].strip.empty?
			return unless @state[:page_response]
			return if @state[:page_response].strip.empty?
			path,query_string = parse_request(@state[:page_request])
			return unless path
			parsed_response = parse_response(@state[:page_response])
			return unless parsed_response
			web_page_info = {}
			web_page_info[:web_site] = @state[:web_site]
			web_page_info[:path] = path
			web_page_info[:code] = parsed_response[:code].to_i
			web_page_info[:headers] = parsed_response[:headers]
			web_page_info[:body] = parsed_response[:body]
			web_page_info[:query] = query_string || ""
			url = ""
			url << @state[:web_site].service.name.to_s << "://"
			url << @state[:web_site].vhost.to_s << ":"
			url << path
			uri = URI.parse(url) rescue nil
			return unless uri # Sanity checker
			db.emit(:web_page, url, &block) if block
			web_page_object = db_report(:web_page,web_page_info)
			@state[:page_request] = @state[:page_response] = nil
			@state[:web_page] = web_page_object
		end

		# Reasons why we shouldn't collect a particular web page.
		def should_skip_this_page
			if @state[:report_item] =~ /Unrestricted File Upload/
				# This means that the page being collected is something the
				# auditor put there, so it's not useful to report on.
				return true
			end
			return false
		end

		# XXX Rex::Proto::Http::Packet seems broken for
		# actually parsing requests and responses, but all I
		# need are the headers anyway
		def parse_request(request)
			headers = Rex::Proto::Http::Packet::Header.new
			headers.from_s request.dup # It's destructive.
			return unless headers.cmd_string
			verb,req = headers.cmd_string.split(/\s+/)
			return unless verb
			return unless req
			path,query_string = req.split(/\?/)[0,2]
		end

		def parse_response(response)
			headers = Rex::Proto::Http::Packet::Header.new
			headers.from_s response.dup # It's destructive.
			return unless headers.cmd_string
			http,code,msg = headers.cmd_string.split(/\s+/)
			return unless code
			return unless code.to_i.to_s == code
			parsed = {}
			parsed[:code] = code
			parsed[:headers] = {}
			headers.each do |k,v|
				parsed[:headers][k.to_s.downcase] = []
				parsed[:headers][k.to_s.downcase] << v
			end
			parsed[:body] = "" # We never seem to get this from Acunetix
			parsed
		end

		# Don't cause the web report to die just because we can't tell
		# what method was used -- default to GET. Sometimes it's just "POST," and
		# sometimes it's "URL encoded POST," and sometimes it might be something
		# else.
		def parse_method(meth)
			verbs = "(GET|POST|PATH)"
			real_method = meth.match(/^\s*#{verbs}/)
			real_method ||= meth.match(/\s*#{verbs}\s*$/)
			( real_method && real_method[1] ) ? real_method[1] : "GET"
		end

		def report_host(&block)
			return unless @report_data[:host]
			return unless in_tag("Scan")
			if host_is_okay
				db.emit(:address,@report_data[:host],&block) if block
				host_info = @report_data.merge(:workspace => @args[:wspace])
				db_report(:host,host_info)
			end
		end

		# The service is super important, so we hang on to it for the
		# rest of the scan.
		def report_starturl_service(host_object,&block)
			return unless host_object
			return unless @state[:starturl_uri]
			name = @state[:starturl_uri].scheme
			port = @state[:starturl_uri].port
			addr = host_object.address
			svc = {
				:host => host_object,
				:port => port,
				:name => name.dup,
				:proto => "tcp"
			}
			if name and port
				db.emit(:service,[addr,port].join(":"),&block) if block
				@state[:starturl_service_object] = db_report(:service,svc)
			end
		end

		def report_web_site(url,&block)
			return unless in_tag("Crawler")
			return unless url
			return if url.strip.empty?
			uri = URI.parse(url) rescue nil
			return unless uri
			host = uri.host
			port = uri.port
			scheme = uri.scheme
			return unless scheme[/^https?/]
			return unless (host && port && scheme)
			address = resolve_address(host)
			return unless address
			service_info = [ @args[:wspace], address, "tcp", port ]
			service_object = db.get_service(*service_info)
			service_object = db_report(:service,service_info) unless service_object
			web_site_info = {
				:workspace => @args[:wspace],
				:service => service_object,
				:vhost => host,
				:ssl => (scheme == "https")
			}
			@state[:web_site] = db_report(:web_site,web_site_info)
			@state[:fullurl] = uri
		end

		def report_starturl_web_site(&block)
			return unless @state[:crawler_starturl]
			starturl = @state[:crawler_starturl].dup
			report_web_site(starturl,&block)
		end

		def report_os_fingerprint
			return unless @state[:starturl_service_object]
			return unless @text
			return if @text.strip.empty?
			return unless in_tag("Scan")
			host = @state[:starturl_service_object].host
			fp_note = {
				:workspace => host.workspace,
				:host => host,
				:type => 'host.os.acunetix_fingerprint',
				:data => {:os => @text}
			}
			db_report(:note, fp_note)
			@text = nil
		end

		def resolve_port(uri)
			@state[:port] = uri.port
			unless @state[:port]
				@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"
			end
			@state[:port] = uri.port
		end

		def resolve_address(host)
			return @resolv_cache[host] if @resolv_cache[host]
			address = Rex::Socket.resolv_to_dotted(host) rescue nil
			@resolv_cache[host] = address
			return address
		end

		def resolve_scan_starturl_address(uri)
			if uri.host
				address = resolve_address(uri.host)
				unless address
					@parse_warnings << "Could not resolve address for '#{uri.host}', skipping '#{@state[:scan_name]}'"
				end
			else
				@parse_warnings << "Could not determine a host for '#{@state[:scan_name]}'"
			end
			address
		end

		def handle_parse_warnings(&block)
			return if @parse_warnings.empty?
			@parse_warnings.each do |pwarn|
				db.emit(:warning, pwarn, &block) if block
			end
		end

	end
	end
end

