module Msf::DBManager::Host
  # TODO: doesn't appear to have any callers. How is this used?
  # Deletes a host and associated data matching this address/comm
  def del_host(wspace, address, comm='')
  ::ApplicationRecord.connection_pool.with_connection {
    address, scope = address.split('%', 2)
    host = wspace.hosts.find_by_address_and_comm(address, comm)
    host.destroy if host
  }
  end

  # Deletes Host entries based on the IDs passed in.
  #
  # @param opts[:ids] [Array] Array containing Integers corresponding to the IDs of the Host entries to delete.
  # @return [Array] Array containing the Mdm::Host objects that were successfully deleted.
  def delete_host(opts)
    raise ArgumentError.new("The following options are required: :ids") if opts[:ids].nil?

    ::ApplicationRecord.connection_pool.with_connection {
      deleted = []
      opts[:ids].each do |host_id|
        host = Mdm::Host.find(host_id)
        begin
          deleted << host.destroy
        rescue # refs suck
          elog("Forcibly deleting #{host.address}")
          deleted << host.delete
        end
      end

      return deleted
    }
  end

  #
  # Iterates over the hosts table calling the supplied block with the host
  # instance of each entry.
  #
  def each_host(wspace=framework.db.workspace, &block)
  ::ApplicationRecord.connection_pool.with_connection {
    wspace.hosts.each do |host|
      block.call(host)
    end
  }
  end

  # Exactly like report_host but waits for the database to create a host and returns it.
  def find_or_create_host(opts)
    host = get_host(opts.clone)
    return host unless host.nil?

    report_host(opts)
  end

  def find_host_by_address_or_id(opts, wspace)
    # Find the host entry in the current workspace by searching on
    # the ID if available. If this isn't possible then try search on
    # the IP address of the host we are currently processing, then save the database
    # entry into the "host" variable.
    if opts[:id]
      host = wspace.hosts.find(opts[:id])
    elsif opts[:address]
      host = wspace.hosts.find_by_address(opts[:address])
    else
      raise ::ArgumentError, 'opts hash did not contain an :id or :address entry!'
    end

    host
  end

  def add_host_tag(opts)
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    tag_name = opts[:tag_name] # This will be the string of the tag that we are using.

    host = find_host_by_address_or_id(opts, wspace)

    # If a host was found
    if host
      # Set host_id to the ID of the host entry in the database that was found.
      host_id = host[:id]

      # Then proceed to go ahead and find potential tags that might have been already
      # created that match the one we are trying to add.
      possible_tags = Mdm::Tag.joins(:hosts).where("hosts.workspace_id = ? and hosts.id = ? and tags.name = ?", wspace.id, host_id, tag_name).order("tags.id DESC").limit(1)

      # If one exists, then use it, otherwise create a new Mdm::Tag, and update
      # the data in the database if the entry was found to need updating (aka the tag
      # hasn't already been applied).
      # @type [Mdm::Tag]
      tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first)
      tag.name = tag_name
      tag.hosts = [host]
      tag.save! if tag.changed?
      tag
    end
  end

  #@todo This will have to be pulled out if tags are used for more than just hosts
  # ATM it will delete the tag from the tag table, not the host<->tag link
  def delete_host_tag(opts)
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    tag_name = opts[:tag_name]
    tag_ids = []

    # If the command line included an address or address range then use this.
    # Otherwise delete all entries that match the given tag.
    host = find_host_by_address_or_id(opts, wspace)
    if host
      found_tags = Mdm::Tag.joins(:hosts).where("hosts.workspace_id = ? and hosts.id = ? and tags.name = ?", wspace.id, host.id, tag_name)
      found_tags.each do |t|
        tag_ids << t.id
      end

      deleted_tags = []

      tag_ids.each do |id|
        tag = Mdm::Tag.find_by_id(id)
        deleted_tags << tag
        tag.destroy
      end

      return deleted_tags
    end
  end

  def get_host_tags(opts)
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    host_id = opts[:id]

    host = wspace.hosts.find(host_id)
    if host
      host.tags
    end
  end

  #
  # Find a host.  Performs no database writes.
  #
  def get_host(opts)
    if opts.kind_of? ::Mdm::Host
      return opts
    elsif opts.kind_of? String
      raise RuntimeError, "This invocation of get_host is no longer supported: #{caller}"
    else
      address = opts[:addr] || opts[:address] || opts[:host] || return
      return address if address.kind_of? ::Mdm::Host
    end
  ::ApplicationRecord.connection_pool.with_connection {
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)

    address = Msf::Util::Host.normalize_host(address)
    return wspace.hosts.find_by_address(address)
  }
  end

  # Returns a list of all hosts in the database
  def hosts(opts)
    ::ApplicationRecord.connection_pool.with_connection {
      # If we have the ID, there is no point in creating a complex query.
      if opts[:id] && !opts[:id].to_s.empty?
        return Array.wrap(Mdm::Host.find(opts[:id]))
      end

      wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)

      conditions = {}
      conditions[:state] = [Msf::HostState::Alive, Msf::HostState::Unknown] if opts[:non_dead]
      conditions[:address] = opts[:address] if opts[:address] && !opts[:address].empty?

      if opts[:search_term] && !opts[:search_term].empty?
        column_search_conditions = Msf::Util::DBManager.create_all_column_search_conditions(Mdm::Host, opts[:search_term])
        tag_conditions = Arel::Nodes::Regexp.new(Mdm::Tag.arel_table[:name], Arel::Nodes.build_quoted("(?mi)#{opts[:search_term]}"))
        search_conditions = column_search_conditions.or(tag_conditions)
        wspace.hosts.where(conditions).where(search_conditions).includes(:tags).references(:tags).order(:address)
      else
        wspace.hosts.where(conditions).order(:address)
      end
    }
  end

  def host_state_changed(host, ostate)
    begin
      framework.events.on_db_host_state(host, ostate)
    rescue ::Exception => e
      wlog("Exception in on_db_host_state event handler: #{e.class}: #{e}")
      wlog("Call Stack\n#{e.backtrace.join("\n")}")
    end
  end

  #
  # Report a host's attributes such as operating system and service pack
  #
  # The opts parameter MUST contain
  # +:host+::         -- the host's ip address
  #
  # The opts parameter can contain:
  # +:state+::        -- one of the Msf::HostState constants
  # +:os_name+::      -- something like "Windows", "Linux", or "Mac OS X"
  # +:os_flavor+::    -- something like "Enterprise", "Pro", or "Home"
  # +:os_sp+::        -- something like "SP2"
  # +:os_lang+::      -- something like "English", "French", or "en-US"
  # +:arch+::         -- one of the ARCHITECTURES listed in metasploit_data_models/app/models/mdm/host.rb
  # +:mac+::          -- the host's MAC address
  # +:scope+::        -- interface identifier for link-local IPv6
  # +:virtual_host+:: -- the name of the virtualization software, eg "VMWare", "QEMU", "Xen", "Docker", etc.
  #
  def report_host(opts)

    return if !active
    addr = opts.delete(:host) || return

    # Sometimes a host setup through a pivot will see the address as "Remote Pipe"
    if addr.eql? "Remote Pipe"
      return
    end

  ::ApplicationRecord.connection_pool.with_connection {
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    opts = opts.clone
    opts.delete(:workspace)

    begin
      retry_attempts ||= 0
      if !addr.kind_of? ::Mdm::Host
        original_addr = addr
        addr = Msf::Util::Host.normalize_host(original_addr)

        unless ipv46_validator(addr)
          raise ::ArgumentError, "Invalid IP address in report_host(): #{original_addr}"
        end

        conditions = {address: addr}
        conditions[:comm] = opts[:comm] if !opts[:comm].nil? && opts[:comm].length > 0
        host = wspace.hosts.where(conditions).first_or_initialize
      else
        host = addr
      end

      ostate = host.state

      # Truncate the info field at the maximum field length
      if opts[:info]
        opts[:info] = opts[:info][0,65535]
      end

      # Truncate the name field at the maximum field length
      if opts[:name]
        opts[:name] = opts[:name][0,255]
      end

      opts.each do |k,v|
        if host.attribute_names.include?(k.to_s)
          unless host.attribute_locked?(k.to_s)
            host[k] = v.to_s.gsub(/[\x00-\x1f]/n, '')
          end
        elsif !v.blank?
          dlog("Unknown attribute for ::Mdm::Host: #{k}")
        end
      end
      host.info = host.info[0,::Mdm::Host.columns_hash["info"].limit] if host.info

      # Set default fields if needed
      host.state = Msf::HostState::Alive if host.state.nil? || host.state.empty?
      host.comm = '' unless host.comm
      host.workspace = wspace unless host.workspace

      begin
        framework.events.on_db_host(host) if host.new_record?
      rescue => e
        wlog("Exception in on_db_host event handler: #{e.class}: #{e}")
        wlog("Call Stack\n#{e.backtrace.join("\n")}")
      end

      host_state_changed(host, ostate) if host.state != ostate

      if host.changed?
        msf_import_timestamps(opts, host)
        host.save!
      end
    rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
      # two concurrent report requests for a new host could result in a RecordNotUnique or
      # RecordInvalid exception, simply retry the report once more as an optimistic approach
      retry if (retry_attempts+=1) <= 1
      raise
    end

    if opts[:task]
      Mdm::TaskHost.create(
          :task => opts[:task],
          :host => host
      )
    end

    host
  }
  end

  def update_host(opts)
    ::ApplicationRecord.connection_pool.with_connection {
      # process workspace string for update if included in opts
      wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework, false)
      opts = opts.clone()
      opts[:workspace] = wspace if wspace

      id = opts.delete(:id)
      host = Mdm::Host.find(id)
      host.update!(opts)
      return host
    }
  end
end
