From b445faab3530b166e1b25613b2595db25c0a8769 Mon Sep 17 00:00:00 2001 From: dmiller Date: Fri, 28 Jun 2019 20:44:19 +0000 Subject: [PATCH] New oops.lua library Loosely inspired by Rustlang's std::Result type: https://doc.rust-lang.org/beta/std/result/index.html This ought to be easy to use to replace uses of `stdnse.format_output(false, ...)` --- CHANGELOG | 3 ++ nselib/comm.lua | 39 ++++++-------- nselib/oops.lua | 129 ++++++++++++++++++++++++++++++++++++++++++++ scripts/daytime.nse | 7 +-- 4 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 nselib/oops.lua 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