local bin = require "bin"
local comm = require "comm"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Gets the time and configuration variables from an NTP server. We send two
requests: a time request and a "read variables" (opcode 2) control message.
Without verbosity, the script shows the time and the value of the
version, processor, system,
refid, and stratum variables. With verbosity, all
variables are shown.
See RFC 1035 and the Network Time Protocol Version 4 Reference and
Implementation Guide
(http://www.eecis.udel.edu/~mills/database/reports/ntp4/ntp4.pdf) for
documentation of the protocol.
]]
---
-- @output
-- PORT    STATE SERVICE VERSION
-- 123/udp open  ntp     NTP v4
-- | ntp-info:  
-- |   receive time stamp: Sat Dec 12 16:22:41 2009
-- |   version: ntpd 4.2.4p4@1.1520-o Wed May 13 21:06:31 UTC 2009 (1)
-- |   processor: x86_64
-- |   system: Linux/2.6.24-24-server
-- |   stratum: 2
-- |_  refid: 195.145.119.188
--
-- @xmloutput
-- 2013-10-18T18:03:05
-- ntpd 4.2.6p3@1.2290-o Tue Jun  5 20:12:11 UTC 2012 (1)
-- i686
-- Linux/3.9.3-24
-- 3
-- 16
-- -20
-- 0.000
-- 2502.720
-- INIT
-- 0x00000000.00000000
-- 0xd60bf655.4cc0ba51
-- 0
-- 3
-- 3
-- 0.000
-- -46.015
-- 0.001
-- 0.000
author = "Richard Sammet"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "discovery", "safe"}
portrule = shortport.port_or_service(123, "ntp", {"udp", "tcp"})
-- This script run against open|filtered ports, so don't wait too long if
-- there's no response.
local TIMEOUT = 5000
-- Only these fields from the response are displayed with default verbosity.
local DEFAULT_FIELDS = {"version", "processor", "system", "refid", "stratum"}
action = function(host, port)
  local status
  local buftres, bufrlres
  local output = stdnse.output_table()
  -- This is a ntp v4 mode3 (client) date/time request.
  local treq = string.char(0xe3, 0x00, 0x04, 0xfa, 0x00, 0x01, 0x00, 0x00, 
                           0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
                           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
                           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
                           0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
                           0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00)
  -- This is a ntp v2 mode6 (control) rl (readlist/READVAR(2)) request. See
  -- appendix B of RFC 1305.
  local rlreq = string.char(0x16, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
                            0x00, 0x00, 0x00, 0x00)
  status, buftres = comm.exchange(host, port, treq, {proto=port.protocol, timeout=TIMEOUT})
  if not status then
    -- Don't try the second probe if this one didn't work.
    return nil
  else
    local _, sec, frac, tstamp
    _, sec, frac = bin.unpack(">II", buftres, 33)
    -- The NTP epoch is 1900-01-01, so subtract 70 years to bring the date into
    -- the range Lua expects. The number of seconds at 1970-01-01 is taken from
    -- the NTP4 reference above.
    tstamp = sec - 2208988800 + frac / 0x10000000
    output["receive time stamp"] = stdnse.format_timestamp(tstamp)
  end
  status, bufrlres = comm.exchange(host, port, rlreq, {proto=port.protocol, timeout=TIMEOUT})
  if status then
    -- This only looks at the first fragment of what can possibly be several
    -- fragments in the response.
    local _, data, k, q, v
    -- Skip the first 10 bytes of the header, then get the data which is
    -- preceded by a 2-byte length.
    _, data = bin.unpack(">P", bufrlres, 11)
    -- This parsing is not quite right with respect to quoted strings.
    -- Backslash escapes should be interpreted inside strings and commas should
    -- be allowed inside them.
    for k, q, v in string.gmatch(data, "%s*(%w+)=(\"?)([^,\"\r\n]*)%2,?") do
      output[k] = v
    end
  end
  if(#output > 0) then
    stdnse.print_debug("Test len: %d", #output)
    nmap.set_port_state(host, port, "open")
    if nmap.verbosity() < 1 then
      local mt = getmetatable(output)
      mt["__tostring"] = function(t)
        local out = {}
        for _,k in ipairs(DEFAULT_FIELDS) do
          if output[k] ~= nil then
            table.insert(out, ("%s: %s"):format(k, output[k]))
          end
        end
        return "\n  " .. table.concat(out, "\n  ")
      end
    end
    return output
  else
    return nil
  end
end