diff --git a/CHANGELOG b/CHANGELOG
index 3076a9c47..b95f66f45 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
#Nmap Changelog ($Id$); -*-text-*-
+o [NSE] New library, oops.lua, makes reporting errors easy, with plenty of
+ debugging detail when needed, and no clutter when not. [Daniel Miller]
+
o [NSE][GH#1126] New script vulners.nse queries the Vulners CVE database API
using CPE information from Nmap's service and application version detection.
[GMedian, Daniel Miller]
diff --git a/nselib/comm.lua b/nselib/comm.lua
index 4115f0df7..4480f69f0 100644
--- a/nselib/comm.lua
+++ b/nselib/comm.lua
@@ -25,6 +25,7 @@
local nmap = require "nmap"
local shortport
local stdnse = require "stdnse"
+local oops = require "oops"
_ENV = stdnse.module("comm", stdnse.seeall)
-- This timeout value (in ms) is added to the connect timeout and represents
@@ -76,7 +77,7 @@ local setup_connect = function(host, port, opts)
if not status then
sock:close()
- return status, err
+ return oops.raise("Could not connect", status, err)
end
sock:set_timeout(request_timeout)
@@ -85,20 +86,15 @@ local setup_connect = function(host, port, opts)
end
local read = function(sock, opts)
- local response, status
-
if opts.lines then
- status, response = sock:receive_lines(opts.lines)
- return status, response
+ return oops.raise("receive_lines failed", sock:receive_lines(opts.lines))
end
if opts.bytes then
- status, response = sock:receive_bytes(opts.bytes)
- return status, response
+ return oops.raise("receive_bytes failed", sock:receive_bytes(opts.bytes))
end
- status, response = sock:receive()
- return status, response
+ return oops.raise("receive failed", sock:receive())
end
--- This function simply connects to the specified port number on the
@@ -115,12 +111,12 @@ end
get_banner = function(host, port, opts)
opts = opts or {}
opts.recv_before = true
- local socket, nothing, correct, banner = tryssl(host, port, "", opts)
+ local socket, errmsg, correct, banner = oops.raise("tryssl failed", tryssl(host, port, nil, opts))
if socket then
socket:close()
return true, banner
end
- return false, banner
+ return false, errmsg
end
--- This function connects to the specified port number on the specified
@@ -143,21 +139,21 @@ exchange = function(host, port, data, opts)
if not status then
-- sock is an error message in this case
- return status, sock
+ return oops.raise("Failed to connect", status, sock)
end
status, ret = sock:send(data)
if not status then
sock:close()
- return status, ret
+ return oops.raise("Failed to send", status, ret)
end
status, ret = read(sock, opts)
sock:close()
- return status, ret
+ return oops.raise("Faield to read", status, ret)
end
--- This function uses shortport.ssl to check if the port is a likely SSL port
@@ -220,22 +216,21 @@ function opencon(host, port, data, opts)
opts = opts or {}
local status, sd = setup_connect(host, port, opts)
if not status then
- return nil, sd, nil
+ return oops.raise("Failed to connect", false, sd)
end
- local response, early_resp;
- if opts.recv_before then status, early_resp = read(sd, opts) end
+ local response, early_resp
+ if opts.recv_before then status, early_resp = oops.raise("read failed", read(sd, opts)) end
if data and #data > 0 then
sd:send(data)
- status, response = sd:receive()
+ status, response = oops.raise("receive failed", sd:receive())
else
response = early_resp
end
if not status then
sd:close()
- return nil, response, early_resp
end
- return sd, response, early_resp
+ return status and sd, response, early_resp
end
--- Opens a SSL connection if possible, with fallback to plain text.
@@ -275,11 +270,11 @@ function tryssl(host, port, data, opts)
stdnse.debug2("DTLS (SSL over UDP) is not supported")
end
opts.proto = opt1
- local sd, response, early_resp = opencon(host, port, data, opts)
+ local sd, response, early_resp = oops.raise(("%s failed"):format(opt1), opencon(host, port, data, opts))
-- Try the second option (If udp, then both options are the same; skip it)
if not sd and opt1 ~= "udp" then
opts.proto = opt2
- sd, response, early_resp = opencon(host, port, data, opts)
+ sd, response, early_resp = oops.raise(("%s failed"):format(opt2), opencon(host, port, data, opts))
best = opt2
end
if not sd then best = "none" end
diff --git a/nselib/oops.lua b/nselib/oops.lua
new file mode 100644
index 000000000..0eafb2bbf
--- /dev/null
+++ b/nselib/oops.lua
@@ -0,0 +1,129 @@
+--- Useful error stack objects
+--
+-- Many NSE library functions return a boolean status and an optional error
+-- message. The Oops library consists of several simple functions to accumulate
+-- these errors and pass them up the stack, resulting in a useful and verbose
+-- error message when debugging.
+--
+-- @author Daniel Miller
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+-- @class module
+-- @name oops
+
+local require = require
+local setmetatable = setmetatable
+local _ENV = require "strict" {}
+
+local nmap = require "nmap"
+local debugging = nmap.debugging
+local verbosity = nmap.verbosity
+
+local table = require "table"
+local concat = table.concat
+local insert = table.insert
+
+local Oops = {
+ new = function (self, message)
+ local o = {message}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ push = function (self, message)
+ insert(self, 1, message)
+ end,
+
+ __tostring = function (self)
+ local banner = "The script encountered an error"
+ local sep = ":\n- "
+ if debugging() > 0 then
+ -- Print full error trace
+ return banner .. sep .. concat(self, sep)
+ end
+ if verbosity() > 0 then
+ -- Show just the top error
+ return banner .. ": " .. self[1]
+ end
+ end,
+}
+
+--- Add an error message to a stack of errors
+--
+-- @param message The error message to add to the stack.
+-- @param previous (Optional) Any error reported by other functions that failed.
+-- @return An Oops object representing the error stack.
+err = function (message, previous)
+ local result
+ if previous then
+ if previous.push then
+ result = previous
+ else
+ result = Oops:new(previous)
+ end
+ result:push(message)
+ elseif message.push then
+ result = message
+ else
+ result = Oops:new(message)
+ end
+ return result
+end
+local err = err
+
+--- Report an error or return a good value
+--
+-- If the status is true, just return the message. If it's false, return the
+-- message as an Oops object. This can be easily used as the final return value
+-- of a script.
+-- @param status The return status of the script.
+-- @param message The output of the script, or an error message if status is false.
+-- @return The message if status is true, or an error message if it is false.
+output = function (status, message)
+ if status then
+ return message
+ else
+ return err(message)
+ end
+end
+local output = output
+
+--- Report a status and error or return values
+--
+-- This is intended to wrap a function that returns a status and either an
+-- error or some value. If the status is false, the message is added to the
+-- stack of errors. Instead of this code:
+--
+--
+-- local status, value_or_error, value = somefunction(args)
+-- if not status then
+-- return status, "somefunction failed for some reason"
+-- end
+--
+--
+-- with this instead:
+--
+--
+-- local status, value_or_error, value = oops.raise("somefunction failed", somefunction(args))
+-- if not status then
+-- return status, value_or_error
+-- end
+--
+--
+-- but instead of just the one error, you get a stack of errors from
+-- somefunction with your own message at the top.
+--
+-- @param message The error message to report if status is false.
+-- @param status The first return value of the function. Treated as boolean, but returned unmodified.
+-- @param previous The second return value of the function, or error.
+-- @return The same status that was input.
+-- @return The rest of the return values, but on error, the message will be added to the stack.
+raise = function (message, status, previous, ...)
+ local r = previous
+ if not status then
+ r = err(message, previous)
+ end
+ return status, r, ...
+end
+
+return _ENV
diff --git a/scripts/daytime.nse b/scripts/daytime.nse
index ff3c2277a..bc8fb2f23 100644
--- a/scripts/daytime.nse
+++ b/scripts/daytime.nse
@@ -1,5 +1,6 @@
local comm = require "comm"
local shortport = require "shortport"
+local oops = require "oops"
description = [[
Retrieves the day and time from the Daytime service.
@@ -21,9 +22,5 @@ categories = {"discovery", "safe"}
portrule = shortport.port_or_service(13, "daytime", {"tcp", "udp"})
action = function(host, port)
- local status, result = comm.exchange(host, port, "dummy", {lines=1})
-
- if status then
- return result
- end
+ return oops.output(comm.exchange(host, port, "dummy", {lines=1}))
end