local ipOps = require "ipOps"
local nmap = require "nmap"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"
local target = require "target"

description = [[
Discovers Sonicwall firewalls which are directly attached (not routed) using
the same method as the manufacturers own 'SetupTool'. An interface needs to be
configured, as the script broadcasts a UDP packet.

The script needs to be run as a privileged user, typically root.

References:
* https://support.software.dell.com/kb/sw3677)
]]

---
-- @usage
-- nmap -e eth0 --script broadcast-sonicwall-discover
--
-- @output
-- | broadcast-sonicwall-discover:
-- |   192.168.5.1
-- |     MAC/Serial: 0006B1001122
-- |     Subnetmask: 255.255.255.0
-- |     Firmware: 3.9.1.2
-- |_    ROM: 14.0.1.1
--
-- @args broadcast-sonicwall-discover.timeout time in seconds to wait for a response
--       (default: 1s)

author = "Raphael Hoegger"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"broadcast", "safe"}


prerule = function()
  if not nmap.is_privileged() then
    stdnse.verbose1("Not running for lack of privileges.")
    return false
  end
  if nmap.address_family() ~= "inet" then
    stdnse.verbose1("Script is IPv4-only")
    return false
  end
  return true
end

action = function(host, port)
  -- preliminary checks
  local interface
  local collect_interface = function (if_table)
    if not interface and if_table.up == "up" and
      if_table.address and if_table.address:match("^%d+%.%d+%.%d+%.%d+$") then
      interface = if_table.device
    end
  end
  stdnse.get_script_interfaces(collect_interface)

  if not interface then
    stdnse.verbose1("No network interface was supplied, aborting.")
    return false
  end
  local sock, co
  sock = nmap.new_socket()

  local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
  timeout = (timeout or 1) * 1000

  -- listen for a response
  sock:set_timeout(timeout)
  sock:pcap_open(interface, 1500, false, "ip && udp && port 26214 && greater 57")
  send_discover()

  local start_time = nmap.clock_ms()
  local results = stdnse.output_table()
  while( nmap.clock_ms() - start_time < timeout ) do
    local status, plen, _, layer3 = sock:pcap_receive()
    -- stop once we picked up our response
    if ( status ) then
      sock:close()
      local p = packet.Packet:new( layer3, #layer3)

      if ( p and p.udp_dport ) then
        -- parsing the result
        local IP = string.sub(layer3:sub(41), 0,4)
        IP = ipOps.str_to_ip(IP)
        local Netmask = string.sub(layer3:sub(45), 0,4)
        Netmask = ipOps.str_to_ip(Netmask)
        local Serial = string.sub(layer3:sub(49), 0,6)
        Serial = stdnse.tohex(Serial)
        local Romversion = string.sub(layer3:sub(55), 0,2)
        local ROMM = stdnse.tohex(Romversion, {separator=".", group=1})
        ROMM = string.gsub(ROMM, "[0-9a-f]", function(n) return tonumber(n, 16) end)
        local Firmwareversion = string.sub(layer3:sub(57), 0,2)
        local FIRMM = stdnse.tohex(Firmwareversion, {separator=".", group=1})
        FIRMM = string.gsub(FIRMM, "[0-9a-f]", function(n) return tonumber(n, 16) end)

        -- add nodes
        if target.ALLOW_NEW_TARGETS then
          target.add(IP)
        end

        local output = stdnse.output_table()
        output['MAC/Serial'] = Serial
        output['Subnetmask'] = Netmask
        output['Firmware'] = FIRMM
        output['ROM Version'] = ROMM
        results[IP] = output
      end
    end
    sock:close()
  end
  if #results > 0 then
    return results
  end
end

function send_discover()
  local host="255.255.255.255"
  local port="26214"
  local socket = nmap.new_socket("udp")

  local status = socket:sendto(host, port, "ackfin ping\00")
  if not status then return end
  socket:close()

  return true
end
