local string = require "string"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"

description = [[
This NSE script will query and parse pcworx protocol to a remote PLC.
The script will send a initial request packets and once a response is received,
it validates that it was a proper response to the command that was sent, and then
will parse out the data. PCWorx is a protocol and Program by Phoenix Contact.


http://digitalbond.com
]]
---
-- @usage
-- nmap --script pcworx-info -p 1962 <host>
--
--
-- @output
--| pcworx-info:
--|   PLC Type: ILC 330 ETH
--|   Model Number: 2737193
--|   Firmware Version: 3.95T
--|   Firmware Date: Mar  2 2012
--|_  Firmware Time: 09:39:02

--
--
-- @xmloutput
--<elem key="PLC Type">ILC 330 ETH</elem>
--<elem key="Model Number">2737193</elem>
--<elem key="Firmware Version">3.95T</elem>
--<elem key="Firmware Date">Mar  2 2012</elem>
--<elem key="Firmware Time">09:39:02</elem>

author = "Stephen Hilt (Digital Bond)"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery"}

portrule = shortport.port_or_service(1962, "pcworx", "tcp")

-- Safely extract a zero-terminated string if the blob is long enough
-- Returns nil if it is not.
local function get_string(blob, offset)
  if #blob >= offset then
    return string.unpack("z", blob, offset)
  end
end
---
--  Action Function that is used to run the NSE. This function will send the initial query to the
--  host and port that were passed in via nmap. The initial response is parsed to determine if host
--  is a pcworx Protocol device. If it is then more actions are taken to gather extra information.
--
-- @param host Host that was scanned via nmap
-- @param port port that was scanned via nmap
action = function(host,port)
  local init_comms = "\x01\x01\0\x1a\0\0\0\0x\x80\0\x03\0\x0cIBETH01N0_M\0"

  -- create table for output
  local output = stdnse.output_table()

  -- create new socket
  local socket = nmap.new_socket()
  -- define the catch of the try statement
  local catch = function()
    socket:close()
  end
  local try = nmap.new_try(catch)

  try(socket:connect(host, port))
  try(socket:send(init_comms))
  local response = try(socket:receive())

  if not response:match("^\x81") then
    stdnse.debug1("Unexpected or unknown PCWorx message.")
    return nil
  end
  -- pcworx has a session ID that is generated by the PLC
  -- This will pull the SID so we can communicate further to the PLC
  local sid = string.sub(response, 18, 18)
  local init_comms2 = "\x01\x05\0\x16\0\x01\0\0\x78\x80\0" .. sid .. "\0\0\0\x06\0\x04\x02\x95\0\0"
  try(socket:send(init_comms2))
  -- receive response
  response = try(socket:receive())
  -- TODO: verify this

  -- this is the request that will pull all the information from the PLC
  local req_info = "\x01\x06\0\x0e\0\x02\0\0\0\0\0" .. sid .. "\x04\0"
  try(socket:send(req_info))
  -- receive response
  response = try(socket:receive())

  -- if the response starts with 0x81 then we will continue
  if not response:match("^\x81") then
    stdnse.debug1("Unexpected or unknown PCWorx message.")
    socket:close()
    return nil
  end

  -- create output table with proper data
  output["PLC Type"] = get_string(response, 31)
  output["Model Number"] = get_string(response, 153)
  output["Firmware Version"] = get_string(response, 67)
  output["Firmware Date"] = get_string(response, 80)
  output["Firmware Time"] = get_string(response, 92)

  -- close socket and return output table
  socket:close()
  return output
end
