1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-26 09:29:01 +00:00

Run rule functions in the main loop.

This allows usage of verbose/debug in portrule/hostrule and access to all
functionality of NSE, including sockets. So for example, we can now do:

function portrule (host, port)
  local response = http.get(host, port, "/");
  stdnse.debug1(response.body)
  ...
end

The verbose/debug function did not work in rule functions because the
introspection API (getid, gettid, etc.) only work when NSE is in the main loop.
The main loop sets the required internal variable current needed by the API.

List of changes:

stdnse.lua:

  o debug/verbose check the debugging/verbosity level much earlier to allow
    returning if nothing will be printed.

  o Simplified debug/verbose logic to handle the optional first argument
    better.

  o made debug/verbose local functions to avoid using globals and allow self
    tail calls

nse_main.lua:

  o The logic for adding threads via a rule function is simplified. So long as
    the script has the desired rule function, a thread is always returned.
    Evaluation of the rule function is done while NSE is in the main loop (i.e.
    not in script:new_thread()). The rule function only determines if the action
    function is run.

  o [Not a change:] If the action function will be run or was run then we see
    the usual "Starting X" and "Finished X" messages from NSE.

  o Use Lua 5.2's pack function instead of the slightly more expensive
    {n = select("#", ...), ...} idiom.

  o New stdnse.getinfo introspection function which is used by stdnse.debug.
This commit is contained in:
batrick
2014-08-02 21:18:08 +00:00
parent 078f3fe108
commit 1cec0a305b
2 changed files with 147 additions and 176 deletions

View File

@@ -52,6 +52,9 @@ local SELECTED_BY_NAME = "NSE_SELECTED_BY_NAME";
local FORMAT_TABLE = "NSE_FORMAT_TABLE";
local FORMAT_XML = "NSE_FORMAT_XML";
-- Unique value indicating the action function is going to run.
local ACTION_STARTING = {};
-- This is a limit on the number of script instance threads running at once. It
-- exists only to limit memory use when there are many open ports. It doesn't
-- count worker threads started by scripts.
@@ -120,6 +123,7 @@ local upper = string.upper;
local table = require "table";
local concat = table.concat;
local insert = table.insert;
local pack = table.pack;
local remove = table.remove;
local sort = table.sort;
local unpack = table.unpack;
@@ -399,7 +403,6 @@ do
-- Register scripts in the timeouts list to track their timeouts.
function Thread:start (timeouts)
self:d("Starting %THREAD_AGAINST.");
if self.host then
timeouts[self.host] = timeouts[self.host] or {};
timeouts[self.host][self.co] = true;
@@ -435,7 +438,7 @@ do
function Script:new_thread (rule, ...)
local script_type = assert(NSE_SCRIPT_RULES[rule]);
if not self[rule] then return nil end -- No rule for this script?
local script_closure_generator = self.script_closure_generator;
-- Rebuild the environment for the running thread.
local env = {
SCRIPT_PATH = self.filename,
@@ -443,50 +446,46 @@ do
SCRIPT_TYPE = script_type,
};
setmetatable(env, {__index = _G});
local script_closure = script_closure_generator(env);
local unique_value = {}; -- to test valid yield
local function main (_ENV, ...)
script_closure(); -- loads script globals
return action(yield(unique_value, _ENV[rule](...)));
local forced = self.forced_to_run;
local script_closure_generator = self.script_closure_generator;
local function main (...)
local _ENV = env; -- change the environment
-- Load the script's globals in the same Lua thread the action and rule
-- functions will execute in.
script_closure_generator(_ENV)();
if forced or _ENV[rule](...) then
yield(ACTION_STARTING)
return action(...)
end
end
-- 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, value, rule_return = resume(co, env, ...);
if s and value ~= unique_value then
print_debug(1,
"A thread for %s yielded unexpectedly in the file or %s function:\n%s\n",
self.filename, rule, traceback(co));
elseif s and (rule_return or self.forced_to_run) then
local thread = {
close_handlers = {},
co = co,
env = env,
identifier = tostring(co),
info = format("'%s' M:%s", self.id, match(tostring(co), "0x(.*)"));
parent = nil, -- placeholder
script = self,
type = script_type,
worker = false,
};
thread.parent = thread;
setmetatable(thread, Thread)
return thread;
elseif not s then
log_error("A thread for %s failed to load in %s function:\n%s\n",
self.filename, rule, traceback(co, tostring(value)));
end
return nil;
local thread = {
action_started = false,
args = pack(...),
close_handlers = {},
co = co,
env = env,
identifier = tostring(co),
info = format("%s M:%s", self.id, match(tostring(co), "0x(.*)"));
parent = nil, -- placeholder
script = self,
type = script_type,
worker = false,
};
thread.parent = thread;
setmetatable(thread, Thread)
return thread;
end
function Thread:new_worker (main, ...)
local co = create(main);
print_debug(2, "%s spawning new thread (%s).", self.parent.info, tostring(co));
local thread = {
args = {n = select("#", ...), ...},
args = pack(...),
close_handlers = {},
co = co,
info = format("'%s' W:%s", self.id, match(tostring(co), "0x(.*)"));
info = format("%s W:%s", self.id, match(tostring(co), "0x(.*)"));
parent = self,
worker = true,
};
@@ -497,8 +496,36 @@ do
return thread, info;
end
function Thread:resume ()
return resume(self.co, unpack(self.args, 1, self.args.n));
function Thread:resume (timeouts)
local ok, r1, r2 = resume(self.co, unpack(self.args, 1, self.args.n));
local status = status(self.co);
if ok and r1 == ACTION_STARTING then
self:d("Starting %THREAD_AGAINST.");
self.action_started = true
return self:resume(timeouts);
elseif not ok then
if debugging() > 0 then
self:d("%THREAD_AGAINST threw an error!\n%s\n", traceback(self.co, tostring(r1)));
else
self:set_output("ERROR: Script execution failed (use -d to debug)");
end
self:close(timeouts, r1);
return false
elseif status == "suspended" then
if r1 == NSE_YIELD_VALUE then
return true
else
self:d("%THREAD yielded unexpectedly and cannot be resumed.");
self:close(timeouts, "yielded unexpectedly and cannot be resumed");
return false
end
elseif status == "dead" then
if self.action_started then
self:set_output(r1, r2);
self:d("Finished %THREAD_AGAINST.");
end
self:close(timeouts);
end
end
function Thread:__index (key)
@@ -833,7 +860,8 @@ local function run (threads_iter, hosts)
-- 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.
-- at once back to running. pending is used because we cannot modify
-- running during traversal.
local running, waiting, pending = {}, {}, {};
local all = setmetatable({}, {__mode = "kv"}); -- base coroutine to Thread
local current; -- The currently running Thread.
@@ -858,7 +886,7 @@ local function run (threads_iter, hosts)
co = base.co;
if waiting[co] then -- ignore a thread not waiting
pending[co], waiting[co] = waiting[co], nil;
pending[co].args = {n = select("#", ...), ...};
pending[co].args = pack(...);
end
end
end
@@ -900,6 +928,9 @@ local function run (threads_iter, hosts)
rawset(stdnse, "getid", function ()
return current and current.id;
end);
rawset(stdnse, "getinfo", function ()
return current and current.info;
end);
rawset(stdnse, "gethostport", function ()
if current then
return current.host, current.port;
@@ -909,20 +940,6 @@ local function run (threads_iter, hosts)
return current and current.worker;
end);
while threads_iter and num_threads < CONCURRENCY_LIMIT do
local thread = threads_iter()
if not thread then
threads_iter = nil;
break;
end
all[thread.co], running[thread.co], total = thread, thread, total+1;
num_threads = num_threads + 1;
thread:start(timeouts);
end
if num_threads == 0 then
return
end
local progress = cnse.scan_progress_meter(NAME);
-- Loop while any thread is running or waiting.
@@ -978,29 +995,10 @@ local function run (threads_iter, hosts)
current, running[co] = thread, nil;
thread:start_time_out_clock();
-- Threads may have zero, one, or two return values.
local s, r1, r2 = thread:resume();
if not s then -- script error...
if thread:resume(timeouts) then
waiting[co] = thread;
else
all[co], num_threads = nil, num_threads-1;
if debugging() > 0 then
thread:d("%THREAD_AGAINST threw an error!\n%s\n", traceback(co, tostring(r1)));
else
thread:set_output("ERROR: Script execution failed (use -d to debug)");
end
thread:close(timeouts, r1);
elseif status(co) == "suspended" then
if r1 == NSE_YIELD_VALUE then
waiting[co] = thread;
else
all[co], num_threads = nil, num_threads-1;
thread:d("%THREAD yielded unexpectedly and cannot be resumed.");
thread:close();
end
elseif status(co) == "dead" then
all[co], num_threads = nil, num_threads-1;
thread:set_output(r1, r2);
thread:d("Finished %THREAD_AGAINST.");
thread:close(timeouts);
end
current = nil;
end
@@ -1316,14 +1314,6 @@ local function main (hosts, scantype)
print_verbose(1, "Script Post-scanning.");
end
-- These functions do not exist until we are executing action functions.
rawset(stdnse, "new_thread", nil)
rawset(stdnse, "base", nil)
rawset(stdnse, "gettid", nil)
rawset(stdnse, "getid", nil)
rawset(stdnse, "gethostport", nil)
rawset(stdnse, "isworker", nil)
for runlevel, scripts in ipairs(runlevels) do
-- This iterator is passed to the run function. It returns one new script
-- thread on demand until exhausted.
@@ -1333,8 +1323,7 @@ local function main (hosts, scantype)
for _, script in ipairs(scripts) do
local thread = script:new_thread("prerule");
if thread then
thread.args = {n = 0};
yield(thread);
yield(thread)
end
end
-- activate hostrule and portrule scripts
@@ -1344,7 +1333,7 @@ local function main (hosts, scantype)
for _, script in ipairs(scripts) do
local thread = script:new_thread("hostrule", host_copy(host));
if thread then
thread.args, thread.host = {n = 1, host_copy(host)}, host;
thread.host = host;
yield(thread);
end
end
@@ -1353,7 +1342,7 @@ local function main (hosts, scantype)
for _, script in ipairs(scripts) do
local thread = script:new_thread("portrule", host_copy(host), tcopy(port));
if thread then
thread.args, thread.host, thread.port = {n = 2, host_copy(host), tcopy(port)}, host, port;
thread.host, thread.port = host, port;
yield(thread);
end
end
@@ -1364,7 +1353,6 @@ local function main (hosts, scantype)
for _, script in ipairs(scripts) do
local thread = script:new_thread("postrule");
if thread then
thread.args = {n = 0};
yield(thread);
end
end

View File

@@ -58,6 +58,34 @@ _ENV = require "strict" {};
-- @usage stdnse.sleep(1.5)
_ENV.sleep = nmap.socket.sleep;
local function debug (level, ...)
if type(level) ~= "number" then
return debug(1, level, ...)
end
local current = nmap.debugging()
if level <= current then
local prefix = "["
if current >= 2 then
prefix = prefix .. (getinfo() or "")
else
prefix = prefix .. (getid() or "")
end
local host, port = gethostport()
if host and host.ip then
prefix = prefix .. " " .. host.ip
end
if port and port.number then
prefix = prefix .. ":" .. port.number
end
prefix = prefix .. "] "
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(...))
else
nmap.log_write("stdout", format(...))
end
end
end
---
-- Prints a formatted debug message if the current debugging level is greater
-- than or equal to a given level.
@@ -72,58 +100,19 @@ _ENV.sleep = nmap.socket.sleep;
-- at least 2, it also prints the base thread identifier and whether it is a
-- worker thread or the master thread.
--
-- @class function
-- @name debug
-- @param level Optional debugging level.
-- @param fmt Format string.
-- @param ... Arguments to format.
function debug (level, fmt, ...)
local current = nmap.debugging()
local prefix = "["
local id = getid()
if id then
prefix = prefix .. id
end
local host, port = gethostport()
if host and host.ip then
prefix = prefix .. " " .. host.ip
end
if port and port.number then
prefix = prefix .. ":" .. port.number
end
local tid = gettid()
local tid = match(tostring(tid), "0x(.*)")
local worker = isworker()
if current >= 2 and tid then
if worker then
prefix = prefix .. " W:" .. tid
else
prefix = prefix .. " M:" .. tid
end
end
prefix = prefix .. "] "
if type(level) == "number" then
if level <= current then
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(fmt, ...))
else
nmap.log_write("stdout", format(fmt, ...))
end
end
elseif 1 <= current then
-- level is fmt and fmt is first argument
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(level, fmt, ...))
else
nmap.log_write("stdout", format(level, fmt, ...))
end
end
end
_ENV.debug = debug
--Aliases for particular debug levels
function debug1 (...) return _ENV.debug(1, ...) end
function debug2 (...) return _ENV.debug(2, ...) end
function debug3 (...) return _ENV.debug(3, ...) end
function debug4 (...) return _ENV.debug(4, ...) end
function debug5 (...) return _ENV.debug(5, ...) end
function debug1 (...) return debug(1, ...) end
function debug2 (...) return debug(2, ...) end
function debug3 (...) return debug(3, ...) end
function debug4 (...) return debug(4, ...) end
function debug5 (...) return debug(5, ...) end
---
-- Deprecated version of debug(), kept for now to prevent the script id from being
@@ -136,7 +125,32 @@ print_debug = function(level, fmt, ...)
nmap.log_write("stdout", format(level, fmt, ...));
end
end
local function verbose (level, ...)
if type(level) ~= "number" then
return verbose(1, level, ...)
end
local current = nmap.verbosity()
if level <= current then
local prefix = "[" .. (getid() or "")
if current >= 2 then
local host, port = gethostport()
if host and host.ip then
prefix = prefix .. " " .. host.ip
end
if port and port.number then
prefix = prefix .. ":" .. port.number
end
end
prefix = prefix .. "] "
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(...))
else
nmap.log_write("stdout", format(...))
end
end
end
---
-- Prints a formatted verbosity message if the current verbosity level is greater
-- than or equal to a given level.
@@ -150,50 +164,19 @@ end
-- identifier. If the verbosity level is at least 2, it also prints the target
-- ip/port (if there is one)
--
-- @class function
-- @name verbose
-- @param level Optional verbosity level.
-- @param fmt Format string.
-- @param ... Arguments to format.
function verbose (level, fmt, ...)
local current = nmap.verbosity()
local prefix = "["
local id = getid()
if id then
prefix = prefix .. id
end
if current >= 2 then
local host, port = gethostport()
if host and host.ip then
prefix = prefix .. " " .. host.ip
end
if port and port.number then
prefix = prefix .. ":" .. port.number
end
end
prefix = prefix .. "] "
if type(level) == "number" then
if level <= current then
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(fmt, ...))
else
nmap.log_write("stdout", format(fmt, ...))
end
end
elseif 1 <= current then
-- level is fmt and fmt is first argument
if prefix ~= "[] " then
nmap.log_write("stdout", prefix..format(level, fmt, ...))
else
nmap.log_write("stdout", format(level, fmt, ...))
end
end
end
_ENV.verbose = verbose
--Aliases for particular verbosity levels
function verbose1 (...) return _ENV.verbose(1, ...) end
function verbose2 (...) return _ENV.verbose(2, ...) end
function verbose3 (...) return _ENV.verbose(3, ...) end
function verbose4 (...) return _ENV.verbose(4, ...) end
function verbose5 (...) return _ENV.verbose(5, ...) end
function verbose1 (...) return verbose(1, ...) end
function verbose2 (...) return verbose(2, ...) end
function verbose3 (...) return verbose(3, ...) end
function verbose4 (...) return verbose(4, ...) end
function verbose5 (...) return verbose(5, ...) end
---
-- Deprecated version of verbose(), kept for now to prevent the script id from being