module Asbestos::Firewall
  module IPTables

    def self.preamble(host)
      [ "# Generated by Asbestos at #{Time.now.utc} for #{host.name}",
        "# #{Asbestos::HOMEPAGE}",
        "*filter"
      ] +
      host.chains.collect do |name, default_action|
        chain name, default_action
      end
    end


    def self.chain(name, default_action)
      default_action = '-' if default_action == :none

      ":#{name.upcase} #{default_action.upcase} [0:0]"
    end


    def self.open_port(interfaces, port, protocol, comment, remote_address = nil)
      if interfaces
        interfaces.collect do |interface|
          accept :state     => :new,
                 :protocol  => protocol,
                 :port      => port,
                 :comment   => comment,
                 :interface => interface,
                 :remote_address => remote_address
        end
      else
        accept :state     => :new,
               :protocol  => protocol,
               :port      => port,
               :comment   => comment,
               :remote_address => remote_address
      end
    end

    #
    # TODO: Use iptables' long options here for clarity?
    #
    def self.rule(args)
      Array.new.tap do |r|
        chain = \
          if args[:chain]
            args[:chain].to_s.upcase
          else
            'INPUT'
          end
        r << "-A #{chain}"

        r << "-j #{args[:action].upcase}" if args[:action]

        if args[:interface]
          direction = \
            if args[:direction]
              case args[:direction]
                when :incoming
                  'i'
                when :outgoing
                  'o'
                else
                  raise "you must provide a :direction flag of either :incoming or :outgoing"
              end
            elsif %w{INPUT PREROUTING}.include? chain
              'i'
            elsif %w{OUTPUT POSTROUTING}.include? chain
              'o'
            else
              raise "you must provide a :direction flag of either :incoming or :outgoing to use :interface with chain #{chain}"
            end
          r << "-#{direction} #{args[:interface]}"
        end

        r << "-p #{args[:protocol]}" if args[:protocol]
        r << "-d #{args[:local_address]}" if args[:local_address]
        r << "-s #{args[:remote_address]}" if args[:remote_address]

        r << "-m state --state #{args[:state].upcase}" if args[:state]
        #r << "-m #{args[:protocol]} --dport #{args[:port]}" if args[:protocol] && args[:port]
        r << "--dport #{args[:port]}" if args[:port]
        r << "-m limit --limit #{args[:limit]}" if args[:limit]

        r << %{--log-prefix "#{args[:log_prefix]}"} if args[:log_prefix]
        r << "--log-level #{args[:log_level]}" if args[:log_level]

        r << "--icmp-type #{args[:icmp_type]}" if args[:icmp_type]

        if args[:comment]
          if args[:interface]
            r << %{-m comment --comment "#{args[:comment]} on #{args[:interface]}"}
          else
            r << %{-m comment --comment "#{args[:comment]}"}
          end
        end
      end.join(' ')
    end

    class << self
      [:accept, :reject, :drop, :log].each do |action|
        define_method(action) do |args|
          self.rule(args.merge(:action => action))
        end
      end
    end



    def self.postamble(host)
      Array.new.tap do |rules|
       rules << log(:limit => '5/min',
                    :log_level => 7,
                    :log_prefix => "iptables dropped: ",
                    :comment => "log dropped packets") if host.log_denials?

       rules << drop(:chain => :input,
                     :comment => "drop packets that haven't been explicitly accepted") if host.chains[:input].upcase == :ACCEPT

       rules << 'COMMIT'
       rules << "# Asbestos completed at #{Time.now.utc}"
      end
    end

  end
end
