-- Arguments when this file (function) is called, accessible via ...
--   [1] The NSE C library. This is saved in the local variable cnse for
--       access throughout the file.
--   [2] The list of categories/files/directories passed via --script.
-- The actual arguments passed to the anonymous main function:
--   [1] The list of hosts we run against.
--
-- A few notes about the safety of the engine, that is, the ability for
-- a script developer to crash or otherwise stall NSE. The purpose of noting
-- these attack vectors is more to show the difficulty in accidently
-- breaking the system than to indicate a user may wish to break the
-- system through these means.
--  - A script writer can use the undocumented Lua function newproxy
--    to inject __gc code that could run (and error) at any location.
--  - A script writer can use the debug library to break out of
--    the "sandbox" we give it. This is made a little more difficult by
--    our use of locals to all Lua functions we use and the exclusion
--    of the main thread and subsequent user threads.
--  - A simple while true do end loop can stall the system. This can be
--    avoided by debug hooks to yield the thread at periodic intervals
--    (and perhaps kill the thread) but a C function like string.find and
--    a malicious pattern can stall the system from C just as easily.
--  - The garbage collector function is available to users and they may
--    cause the system to stall through improper use.
--  - Of course the os and io library can cause the system to also break.

local NAME = "NSE";

local _R = debug.getregistry(); -- The registry
local _G = _G;

local assert = assert;
local collectgarbage = collectgarbage;
local error = error;
local getfenv = getfenv;
local ipairs = ipairs;
local loadfile = loadfile;
local loadstring = loadstring;
local next = next;
local pairs = pairs;
local pcall = pcall;
local rawget = rawget;
local select = select;
local setfenv = setfenv;
local setmetatable = setmetatable;
local tonumber = tonumber;
local tostring = tostring;
local type = type;
local unpack = unpack;

local create = coroutine.create;
local resume = coroutine.resume;
local running = coroutine.running;
local status = coroutine.status;
local yield = coroutine.yield;

local traceback = debug.traceback;

local write = io.write;

local ceil = math.ceil;

local byte = string.byte;
local format = string.format;
local find = string.find;
local gsub = string.gsub;
local lower = string.lower;
local match = string.match;

local insert = table.insert;
local remove = table.remove;
local sort = table.sort;

local nmap = require "nmap";

local cnse, rules = ...; -- The NSE C library and Script Rules

do -- Append the nselib directory to the Lua search path
  local t, path = assert(cnse.fetchfile_absolute("nselib/"));
  assert(t == "directory", "could not locate nselib directory!");
  package.path = package.path..";"..path.."?.lua";
end

-- Some local helper functions --

local log_write, verbosity, debugging =
    nmap.log_write, nmap.verbosity, nmap.debugging;

local function print_verbose (level, fmt, ...)
  if verbosity() >= assert(tonumber(level)) or debugging() > 0 then
    log_write("stdout", format(fmt, ...));
  end
end

local function print_debug (level, fmt, ...)
  if debugging() >= assert(tonumber(level)) then
    log_write("stdout", format(fmt, ...));
  end
end

local function log_error (fmt, ...)
  log_write("stderr", format(fmt, ...));
end

local function table_size (t)
  local n = 0; for _ in pairs(t) do n = n + 1; end return n;
end

-- recursively copy a table, for host/port tables
-- not very rigorous, but it doesn't need to be
local function tcopy (t)
  local tc = {};
  for k,v in pairs(t) do
    if type(v) == "table" then
      tc[k] = tcopy(v);
    else
      tc[k] = v;
    end
  end
  return tc;
end

local Script = {}; -- The Script Class, its constructor is Script.new.
local Thread = {}; -- The Thread Class, its constructor is Script:new_thread.
do
  -- Thread:d()
  -- Outputs debug information at level 1 or higher.
  -- Changes "%THREAD" with an appropriate identifier for the debug level
  function Thread:d (fmt, ...)
    if debugging() > 1 then
      print_debug(1, gsub(fmt, "%%THREAD", self.info), ...);
    else
      print_debug(1, gsub(fmt, "%%THREAD", self.short_basename), ...);
    end
  end

  -- thread = Script:new_thread(rule, ...)
  -- Creates a new thread for the script Script.
  -- Arguments:
  --   rule  The rule argument the rule, hostrule or portrule, tested.
  --   ...   The arguments passed to the rule function (host[, port]).
  -- Returns:
  --   thread  The thread (class) is returned, or nil.
  function Script:new_thread (rule, ...)
    assert(rule == "hostrule" or rule == "portrule");
    if not self[rule] then return nil end -- No rule for this script?
    local file_closure = self.file_closure;
    local env = setmetatable({
        runlevel = 1,
        filename = self.filename,
      }, {__index = _G});
    setfenv(file_closure, env);
    local function main (...)
      file_closure(); -- loads script globals
      return env.action(yield(env[rule](...)));
    end
    setfenv(main, env);
    -- This thread allows us to load the script's globals in the
    -- same Lua thread the action and rule functions will execute in.
    local co = create(main);
    local s, rule_return = resume(co, ...);
    if s and rule_return then
      local thread = setmetatable({
        co = co,
        env = env,
        runlevel = ceil(tonumber(rawget(env, "runlevel")) or 1),
        identifier = tostring(co),
        info = format("'%s' (%s)", self.short_basename, tostring(co));
        type = rule == "hostrule" and "host" or "port",
      }, {
        __metatable = Thread,
        __index = function (thread, k) return Thread[k] or self[k] end
      }); -- Access to the parent Script
      thread.parent = thread; -- itself
      return thread;
    elseif not s then
      print_debug(1, "a thread for %s failed to load:\n%s\n", self.filename,
          traceback(co, tostring(rule_return)));
    end
    return nil;
  end

  local required_fields = {
    description = "string",
    action = "function",
    categories = "table",
  };
  -- script = Script.new(filename)
  -- Creates a new Script Class for the script.
  -- Arguments:
  --   filename  The filename (path) of the script to load.
  -- Returns:
  --   script  The script (class) created.
  function Script.new (filename)
    assert(type(filename) == "string", "string expected");
    if not find(filename, "%.nse$") then
      log_error("Warning: Loading '"..filename..
          "' - the recommended file extension is '.nse'.");
    end
    local file_closure = assert(loadfile(filename));
    -- Give the closure its own environment, with global access
    local env = setmetatable({}, {__index = _G});
    setfenv(file_closure, env);
    local co = create(file_closure); -- Create a garbage thread
    assert(resume(co)); -- Get the globals it loads in env
    -- Check that all the required fields were set
    for f, t in pairs(required_fields) do
      local field = rawget(env, f);
      if field == nil then
        error(filename.." is missing required field: '"..f.."'");
      elseif type(field) ~= t then
        error(filename.." field '"..f.."' is of improper type '"..
            type(field).."', expected type '"..t.."'");
      end
    end
    -- Check one of two required rule functions exists
    local hostrule, portrule = rawget(env, "hostrule"), rawget(env, "portrule");
    assert(type(hostrule) == "function" or type(portrule) == "function",
        filename.." is missing a required function: 'hostrule' or 'portrule'");
    -- Assert that categories is an array of strings
    for i, category in ipairs(rawget(env, "categories")) do
      assert(type(category) == "string", 
        filename.." has non-string entries in the 'categories' array");
    end
    -- Return the script
    return setmetatable({
      filename = filename,
      basename = match(filename, "[/\\]([^/\\]-)$") or filename,
      short_basename = match(filename, "[/\\]([^/\\]-)%.nse$") or
                       match(filename, "[/\\]([^/\\]-)%.[^.]*$") or
                       filename,
      id = match(filename, "^.-[/\\]([^\\/]-)%.nse$") or filename,
      file_closure = file_closure,
      hostrule = type(hostrule) == "function" and hostrule or nil,
      portrule = type(portrule) == "function" and portrule or nil,
      args = {n = 0};
      categories = rawget(env, "categories"),
      author = rawget(env, "author"),
      license = rawget(env, "license"),
      runlevel = ceil(tonumber(rawget(env, "runlevel")) or 1),
      threads = {},
    }, {__index = Script, __metatable = Script});
  end
end

-- check_rules(rules)
-- Ensures reserved rules are not explicitly specified.
-- Adds the "default" category if no rules were specified.
-- Adds reserved rules that were internally specified (--sV for "version").
--
-- Arguments:
--   rules  The array of rules to check.
local function check_rules (rules)
  local reserved = {
    version = not not cnse.scriptversion,
  };
  for i, rule in ipairs(rules) do
    if reserved[lower(rule)] ~= nil then
      error("explicitly specifying rule '"..rule.."' is prohibited");
    end
  end
  if cnse.default and #rules == 0 then rules[1] = "default"; end
  for rule, option in pairs(reserved) do
    if option then rules[#rules+1] = rule; end
  end
end

-- chosen_scripts = get_chosen_scripts(rules)
-- Loads all the scripts for the given rules using the Script Database.
-- Arguments:
--   rules  The array of rules to use for loading scripts.
-- Returns:
--   chosen_scripts  The array of scripts loaded for the given rules. 
local function get_chosen_scripts (rules)
  check_rules(rules);

  local script_dbpath = cnse.script_dbpath;
  local t, path = cnse.fetchfile_absolute(script_dbpath);
  if not t then
    print_verbose(1, "Creating non-existent script database.");
    assert(cnse.updatedb(), "could not update script database!");
    t, path = assert(cnse.fetchfile_absolute(script_dbpath));
  end
  local db_closure = assert(loadfile(path));

  local chosen_scripts, entry_rules, files_loaded = {}, {}, {};

  -- Initialize entry_rules with the list of rules provided by the user.
  -- Each element of entry_rules may refer to another canonical element.
  -- Here the lower-case rule points to the potentially mixed-case rule
  -- provided by the user.
  for i, rule in ipairs(rules) do
    entry_rules[lower(rule)] = rule;
    entry_rules[rule] = false;
  end

  -- Start by loading scripts by category. This function is run on each
  -- Entry in script.db.
  local function entry (script_entry)
    local category, filename = script_entry.category, script_entry.filename;
    assert(type(category) == "string" and type(filename) == "string");

    -- Don't load a file more than once.
    if files_loaded[filename] then return end

    -- Do we have a rule for this category (or an "all" rule)?
    if entry_rules[category] ~= nil or
        entry_rules.all ~= nil and category ~= "version" then
      local index = entry_rules[category] ~= nil and category or "all";
      local mark = entry_rules[index];
      -- mark may point to the actual mixed case category passed via command
      -- line
      if type(mark) == "boolean" then
        entry_rules[index] = true;
      else
        entry_rules[mark] = true;
      end
      local t, path = cnse.fetchfile_absolute(filename);
      assert(t == "file", filename.." is not a file!");
      chosen_scripts[#chosen_scripts+1] = Script.new(path);
      files_loaded[filename] = true;
    end
  end

  setfenv(db_closure, {Entry = entry});
  db_closure(); -- Load the scripts

  -- Now load any scripts listed by name rather than by category.
  for rule, loaded in pairs(entry_rules) do
    if not loaded then -- attempt to load the file/directory
      local t, path = cnse.fetchfile_absolute(rule);
      if t == nil then -- perhaps omitted the extension?
        t, path = cnse.fetchfile_absolute(rule..".nse");
      end
      if t == nil then
        error("No such category, filename or directory: '"..rule.."'");
      elseif t == "file" and not files_loaded[path] then
        chosen_scripts[#chosen_scripts+1] = Script.new(path);
        files_loaded[path] = true;
      elseif t == "directory" then
        for i, file in ipairs(cnse.dump_dir(path)) do
          if not files_loaded[file] then
            chosen_scripts[#chosen_scripts+1] = Script.new(file);
            files_loaded[file] = true;
          end
        end
      end
    end
  end
  return chosen_scripts;
end

-- run(threads)
-- The main loop function for NSE. It handles running all the script threads.
-- Arguments:
--   threads  An array of threads (a runlevel) to run.
local function run (threads)
  -- running scripts may be resumed at any time. waiting scripts are
  -- yielded until Nsock wakes them. After being awakened with
  -- nse_restore, waiting threads become pending and later are moved all
  -- at once back to running.
  local running, waiting, pending = {}, {}, {}
  -- hosts maps a host to a list of threads for that host.
  local hosts, total = {}, 0
  local current
  local progress = cnse.scan_progress_meter(NAME);

  print_debug(1, "NSE Script Threads (%d) running:", #threads);
  while #threads > 0 do
    local thread = remove(threads);
    thread:d("Starting %THREAD against %s.", thread.host.ip)
    running[thread.co], total = thread, total + 1;
    hosts[thread.host] = hosts[thread.host] or {};
    hosts[thread.host][thread.co] = true;
  end

  -- This WAITING_TO_RUNNING function is called by nse_restore in
  -- nse_main.cc.
  _R.WAITING_TO_RUNNING = function (co, ...)
    if waiting[co] then -- ignore a thread not waiting
      pending[co], waiting[co] = waiting[co], nil;
      pending[co].args = {n = select("#", ...), ...};
    end
  end

  -- Loop while any thread is running or waiting.
  while next(running) or next(waiting) do
    local nr, nw = table_size(running), table_size(waiting);
    cnse.nsock_loop(50); -- Allow nsock to perform any pending callbacks
    if cnse.key_was_pressed() then
      print_verbose(1, "Active NSE Script Threads: %d (%d waiting)\n",
          nr+nw, nw);
      progress("printStats", 1-(nr+nw)/total);
    elseif progress "mayBePrinted" then
      if verbosity() > 1 or debugging() > 0 then
        progress("printStats", 1-(nr+nw)/total);
      else
        progress("printStatsIfNecessary", 1-(nr+nw)/total);
      end
    end

    -- Checked for timed-out hosts.
    for co, thread in pairs(waiting) do
      if cnse.timedOut(thread.host) then
        waiting[co] = nil;
        thread:d("%THREAD target timed out");
      end
    end

    for co, thread in pairs(running) do
      current, running[co] = thread, nil;
      cnse.startTimeOutClock(thread.host);

      local s, result = resume(co, unpack(thread.args, 1, thread.args.n));
      if not s then -- script error...
        hosts[thread.host][co] = nil;
        thread:d("%THREAD threw an error!\n%s\n",
            traceback(co, tostring(result)));
      elseif status(co) == "suspended" then
        waiting[co] = thread;
      elseif status(co) == "dead" then
        hosts[thread.host][co] = nil;
        if type(result) == "string" then
          -- Escape any character outside the range 32-126 except for tab,
          -- carriage return, and line feed. This makes the string safe for
          -- screen display as well as XML (see section 2.2 of the XML spec).
          result = gsub(result, "[^\t\r\n\032-\126]", function(a)
            return format("\\x%02X", byte(a));
          end);
          if thread.type == "host" then
            cnse.host_set_output(thread.host, thread.id, result);
          else
            cnse.port_set_output(thread.host, thread.port, thread.id, result);
          end
        end
        thread:d("Finished %THREAD against %s", thread.host.ip);
      end

      -- Any more threads running for this host?
      if not next(hosts[thread.host]) then
        cnse.stopTimeOutClock(thread.host);
      end
    end

    -- Move pending threads back to running.
    for co, thread in pairs(pending) do
      pending[co], running[co] = nil, thread;
    end

    collectgarbage "collect"; -- important for collecting used sockets & proxies
  end

  progress "endTask";
end

do -- Load script arguments
  local args = gsub((cnse.scriptargs or ""), "=([%w_]+)", "=\"%1\"");
  local argsf, err = loadstring("return {"..args.."}", "Script Arguments");
  if not argsf then
    error("failed to parse --script-args:\n"..args.."\n"..err);
  else
    nmap.registry.args = argsf();
  end
end

-- Load all user chosen scripts
local chosen_scripts = get_chosen_scripts(rules);
print_verbose(1, "Loaded %d scripts for scanning.", #chosen_scripts);
for i, script in ipairs(chosen_scripts) do
  print_debug(2, "Loaded '%s'.", script.basename);
end

-- main(hosts)
-- This is the main function we return to NSE (on the C side) which actually
-- runs a scan against an array of hosts. nse_main.cc gets this function
-- by calling loadfile on nse_main.lua.
-- Arguments:
--   hosts  An array of hosts to scan.
return function (hosts)
  if #hosts > 1 then
    print_verbose(1, "Script scanning %d hosts.", #hosts);
  elseif #hosts == 1 then
    print_verbose(1, "Script scanning %s.", hosts[1].ip);
  end

  -- Set up the runlevels.
  local threads, runlevels = {}, {};
  for j, host in ipairs(hosts) do
    -- Check hostrules for this host.
    for i, script in ipairs(chosen_scripts) do
      local thread = script:new_thread("hostrule", tcopy(host));
      if thread then
        local runlevel = thread.runlevel;
        if threads[runlevel] == nil then insert(runlevels, runlevel); end
        threads[runlevel] = threads[runlevel] or {};
        insert(threads[runlevel], thread);
        thread.args, thread.host = {n = 1, tcopy(host)}, host;
      end
    end
    -- Check portrules for this host.
    for port in cnse.ports(host) do
      for i, script in ipairs(chosen_scripts) do
        local thread = script:new_thread("portrule", tcopy(host),
            tcopy(port));
        if thread then
          local runlevel = thread.runlevel;
          if threads[runlevel] == nil then insert(runlevels, runlevel); end
          threads[runlevel] = threads[runlevel] or {};
          insert(threads[runlevel], thread);
          thread.args, thread.host, thread.port =
              {n = 2, tcopy(host), tcopy(port)}, host, port;
        end
      end
    end
  end

  sort(runlevels);
  for i, runlevel in ipairs(runlevels) do
    print_verbose(1, "Starting runlevel %s scan", tostring(runlevel));
    run(threads[runlevel]);
  end

  collectgarbage "collect";
  print_verbose(1, "Script Scanning completed.");
end
