424 lines
11 KiB
Lua

---
-- A minimalistic NDMP (Network Data Management Protocol) library
--
-- @author Patrik Karlsson <patrik@cqure.net>
--
local match = require "match"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("ndmp", stdnse.seeall)
NDMP = {
-- Message types
MessageType = {
CONFIG_GET_HOST_INFO = 0x00000100,
CONFIG_GET_FS_INFO = 0x00000105,
CONFIG_GET_AUTH_ATTR = 0x00000103,
CONFIG_GET_SERVER_INFO = 0x00000108,
CONNECT_CLIENT_AUTH = 0x00000901,
},
-- Error types
ErrorType = {
NOT_AUTHORIZED_ERROR = 0x00000004
},
-- The fragment header, 4 bytes where the highest bit is used to determine
-- whether the fragment is the last or not.
FragmentHeader = {
size = 4,
-- Creates a new instance of fragment header
-- @return o instance of Class
new = function(self)
local o = {
last = true,
length = 24,
}
setmetatable(o, self)
self.__index = self
return o
end,
-- Parse data stream and create a new instance of this class
-- @param data opaque string
-- @return fh new instance of FragmentHeader class
parse = function(data)
local fh = NDMP.FragmentHeader:new()
local tmp = string.unpack(">I4", data)
fh.length = (tmp & 0x7fffffff)
fh.last= (tmp >> 31)
return fh
end,
-- Serializes the instance to an opaque string
-- @return str string containing the serialized class
__tostring = function(self)
local tmp = 0
if ( self.last ) then
tmp = 0x80000000
end
tmp = tmp + self.length
return string.pack(">I4", tmp)
end,
},
-- The ndmp 24 byte header
Header = {
size = 24,
-- creates a new instance of Header
-- @return o instance of Header
new = function(self)
local o = {
seq = 0,
time = os.time(),
type = 0,
msg = 0x00000108,
reply_seq = 0,
error = 0,
}
setmetatable(o, self)
self.__index = self
return o
end,
-- Create a Header instance from opaque data string
-- @param data opaque string
-- @return hdr new instance of Header
parse = function(data)
local hdr = NDMP.Header:new()
hdr.seq, hdr.time, hdr.type, hdr.msg, hdr.reply_seq, hdr.error = string.unpack(">I4I4I4I4I4I4", data)
return hdr
end,
-- Serializes the instance to an opaque string
-- @return str string containing the serialized class instance
__tostring = function(self)
return string.pack(">I4I4I4I4I4I4", self.seq, self.time, self.type, self.msg, self.reply_seq, self.error)
end,
},
}
NDMP.Message = {}
NDMP.Message.ConfigGetServerInfo = {
-- Creates a Config Server Info instance
-- @return o new instance of Class
new = function(self)
local o = {
frag_header = NDMP.FragmentHeader:new(),
header = NDMP.Header:new(),
data = nil,
}
o.header.msg = NDMP.MessageType.CONFIG_GET_SERVER_INFO
setmetatable(o, self)
self.__index = self
return o
end,
-- Create a ConfigGetServerInfo instance from opaque data string
-- @param data opaque string
-- @return msg new instance of ConfigGetServerInfo
parse = function(data)
local msg = NDMP.Message.ConfigGetServerInfo:new()
msg.frag_header = NDMP.FragmentHeader.parse(data)
data = data:sub(NDMP.FragmentHeader.size + 1)
msg.header = NDMP.Header.parse(data)
msg.data = data:sub(NDMP.Header.size + 1)
msg.serverinfo = {}
local err, pos = string.unpack(">I4", msg.data)
pos, msg.serverinfo.vendor = Util.parseString(msg.data, pos)
pos, msg.serverinfo.product = Util.parseString(msg.data, pos)
pos, msg.serverinfo.version = Util.parseString(msg.data, pos)
return msg
end,
-- Serializes the instance to an opaque string
-- @return str string containing the serialized class instance
__tostring = function(self)
return tostring(self.frag_header) .. tostring(self.header) .. tostring(self.data or "")
end,
}
NDMP.Message.ConfigGetHostInfo = {
new = function(self)
local o = {
frag_header = NDMP.FragmentHeader:new(),
header = NDMP.Header:new(),
data = nil,
}
o.header.msg = NDMP.MessageType.CONFIG_GET_HOST_INFO
setmetatable(o, self)
self.__index = self
return o
end,
parse = function(data)
local msg = NDMP.Message.ConfigGetServerInfo:new()
msg.frag_header = NDMP.FragmentHeader.parse(data)
data = data:sub(NDMP.FragmentHeader.size + 1)
msg.header = NDMP.Header.parse(data)
if ( msg.header.error == NDMP.ErrorType.NOT_AUTHORIZED_ERROR ) then
-- no data to parse
return msg
end
msg.data = data:sub(NDMP.Header.size + 1)
msg.hostinfo = {}
local err, pos = string.unpack(">I4", msg.data)
pos, msg.hostinfo.hostname = Util.parseString(msg.data, pos)
pos, msg.hostinfo.ostype = Util.parseString(msg.data, pos)
pos, msg.hostinfo.osver = Util.parseString(msg.data, pos)
pos, msg.hostinfo.hostid = Util.parseString(msg.data, pos)
return msg
end,
__tostring = function(self)
return tostring(self.frag_header) .. tostring(self.header) .. tostring(self.data or "")
end,
}
NDMP.Message.ConfigGetFsInfo = {
new = function(self)
local o = {
frag_header = NDMP.FragmentHeader:new(),
header = NDMP.Header:new(),
data = nil,
fsinfo = {},
}
o.header.msg = NDMP.MessageType.CONFIG_GET_FS_INFO
setmetatable(o, self)
self.__index = self
return o
end,
parse = function(data)
local msg = NDMP.Message.ConfigGetFsInfo:new()
msg.frag_header = NDMP.FragmentHeader.parse(data)
data = data:sub(NDMP.FragmentHeader.size + 1)
msg.header = NDMP.Header.parse(data)
if ( msg.header.error == NDMP.ErrorType.NOT_AUTHORIZED_ERROR ) then
-- no data to parse
return msg
end
msg.data = data:sub(NDMP.Header.size + 1)
local err, count, pos = string.unpack(">I4I4", msg.data)
for i=1, count do
local item = {}
item.invalid, pos = string.unpack(">I4", msg.data, pos)
pos, item.fs_type = Util.parseString(msg.data, pos)
pos, item.fs_logical_device = Util.parseString(msg.data, pos)
pos, item.fs_physical_device = Util.parseString(msg.data, pos)
item.total_size,
item.used_size,
item.avail_size,
item.total_inodes,
item.used_inodes, pos = string.unpack(">I8I8I8I8I8", msg.data, pos)
pos, item.fs_env = Util.parseString(msg.data, pos)
pos, item.fs_status = Util.parseString(msg.data, pos)
table.insert(msg.fsinfo, item)
end
return msg
end,
__tostring = function(self)
return tostring(self.frag_header) .. tostring(self.header) .. tostring(self.data or "")
end,
}
NDMP.Message.UnhandledMessage = {
new = function(self)
local o = {
frag_header = NDMP.FragmentHeader:new(),
header = NDMP.Header:new(),
data = nil,
}
setmetatable(o, self)
self.__index = self
return o
end,
parse = function(data)
local msg = NDMP.Message.ConfigGetFsInfo:new()
msg.frag_header = NDMP.FragmentHeader.parse(data)
data = data:sub(NDMP.FragmentHeader.size + 1)
msg.header = NDMP.Header.parse(data)
msg.data = data:sub(NDMP.Header.size + 1)
return msg
end,
__tostring = function(self)
return tostring(self.frag_header) .. tostring(self.header) .. tostring(self.data or "")
end
}
Util = {
parseString = function(data, pos)
local str, pos = string.unpack(">s4", data, pos)
local pad = ( 4 - ( #str % 4 ) ~= 4 ) and 4 - ( #str % 4 ) or 0
return pos + pad, str
end,
}
NDMP.TypeToMessage = {
[NDMP.MessageType.CONFIG_GET_SERVER_INFO] = NDMP.Message.ConfigGetServerInfo,
[NDMP.MessageType.CONFIG_GET_HOST_INFO] = NDMP.Message.ConfigGetHostInfo,
[NDMP.MessageType.CONFIG_GET_FS_INFO] = NDMP.Message.ConfigGetFsInfo,
}
-- Handles the communication with the NDMP service
Comm = {
-- Creates new Comm instance
-- @param host table as received by the action method
-- @param port table as received by the action method
-- @return o new instance of Comm
new = function(self, host, port)
local o = {
host = host,
port = port,
socket = nmap.new_socket(),
seq = 0,
in_queue = {},
}
setmetatable(o, self)
self.__index = self
return o
end,
-- Connects to the NDMP server
-- @return status true on success, false on failure
connect = function(self)
-- some servers seem to take their time, so leave this as 10s for now
self.socket:set_timeout(10000)
return self.socket:connect(self.host, self.port)
end,
-- Receives a message from the server
-- @return status true on success, false on failure
-- @return msg NDMP message when a parser exists, otherwise nil
sock_recv = function(self)
local status, frag_data = self.socket:receive_buf(match.numbytes(4), true)
if ( not(status) ) then
return false, "Failed to read NDMP 4-byte fragment header"
end
local frag_header = NDMP.FragmentHeader.parse(frag_data)
local status, header_data = self.socket:receive_buf(match.numbytes(24), true)
if ( not(status) ) then
return false, "Failed to read NDMP 24-byte header"
end
local header = NDMP.Header.parse(header_data)
local status, data = self.socket:receive_buf(match.numbytes(frag_header.length - 24), true)
if ( not(status) ) then
return false, "Failed to read NDMP data"
end
if ( NDMP.TypeToMessage[header.msg] ) then
return true, NDMP.TypeToMessage[header.msg].parse(frag_data .. header_data .. data)
end
return true, NDMP.Message.UnhandledMessage.parse(frag_data .. header_data .. data)
end,
recv = function(self)
if ( #self.in_queue > 0 ) then
return true, table.remove(self.in_queue, 1)
end
return self:sock_recv()
end,
-- Sends a message to the server
-- @param msg NDMP message
-- @return status true on success, false on failure
-- @return err string containing the error message when status is false
send = function(self, msg)
self.seq = self.seq + 1
msg.header.seq = self.seq
return self.socket:send(tostring(msg))
end,
exch = function(self, msg)
local status, err = self:send(msg)
if ( not(status) ) then
return false, "Failed to send ndmp Message to server"
end
local s_seq = msg.header.seq
for k, v in ipairs(self.in_queue) do
if ( v.reply_seq == s_seq ) then
return true, table.remove(self.in_queue, k)
end
end
while(true) do
local reply
status, reply = self:sock_recv()
if ( not(status) ) then
return false, "Failed to receive msg from server"
elseif ( reply and reply.header and reply.header.reply_seq == s_seq ) then
return true, reply
else
table.insert(self.in_queue, reply)
end
end
end,
close = function(self) return self.socket:close() end,
}
Helper = {
new = function(self, host, port)
local o = { comm = Comm:new(host, port) }
setmetatable(o, self)
self.__index = self
return o
end,
connect = function(self)
return self.comm:connect()
end,
getFsInfo = function(self)
return self.comm:exch(NDMP.Message.ConfigGetFsInfo:new())
end,
getHostInfo = function(self)
return self.comm:exch(NDMP.Message.ConfigGetHostInfo:new())
end,
getServerInfo = function(self)
return self.comm:exch(NDMP.Message.ConfigGetServerInfo:new())
end,
close = function(self)
return self.comm:close()
end
}
return _ENV;