mirror of
https://github.com/vxcontrol/soldr-modules.git
synced 2026-07-01 12:47:17 -04:00
986 lines
41 KiB
Lua
986 lines
41 KiB
Lua
---------------------------------------------------
|
|
-- MOCK API description
|
|
--[===============================================[
|
|
__mock - global object which contains internal fields to mock vxlua API and must be initialize before
|
|
__mock.is_closed - tells if module need to be closed
|
|
__mock.vars - table (dict) is a local storage between scenario and all receive callbacks
|
|
__mock.stage - table (dict) is a state machine storage to use it between scenario and all receive callbacks
|
|
__mock.stage.ctx - table (dict) is storage of collected date from send_* api call from the module side
|
|
__mock.stage.time - number is a last time (number of seconds) when the state machine changed its step
|
|
__mock.stage.coro - coroutine for main scenario of the test script
|
|
__mock.timeout - number is a maximum number of seconds in step before the state machine will raise timeout error
|
|
__mock.module - string is the module name which going to load into the lua state for testing (module/version)
|
|
__mock.version - string is the module version which going to load into the lua state for testing (module/version)
|
|
__mock.side - string is the module code folder with main lua (must be "server" or "agent")
|
|
__mock.cwd - string is a path to temporary current directory to which cwd will be changed after loading MOCK API
|
|
__mock.tmpdir - string is a path to temporary/flushable directory which contains "data" and "clibs" files
|
|
__mock.base_path - string is a path modules folder where going to looking for module path; default is current dir
|
|
__mock.log_level - string is logging level which will use for testing: ["error", "warn", "info", "debug", "trace"]
|
|
__mock.scenario - function is a main script other side which can check the state and can send packets into the state
|
|
__mock.module_callbacks - registered list of module callbacks (populated by __api.add_cbs)
|
|
__mock.callbacks - table (list) of functions which will called from the lua state to notify scenario side
|
|
data(self, dst, src, data) - function to notify scenario when module want to send data to another side
|
|
file(self, dst, src, path, name) - function to notify scenario when module want to send file to another side
|
|
text(self, dst, src, text, name) - function to notify scenario when module want to send text to another side
|
|
msg(self, dst, src, msg, mtype) - function to notify scenario when module want to send msg to another side
|
|
action(self, dst, src, data, name) - function to notify scenario when module want to send action to another side
|
|
push_event(self, aid, event) - function to notify scenario when module want to log event into local DB
|
|
trace(func_name, ...) - function to notify scenario when module called some method from vxapi
|
|
__mock.os - table (dict) is a config OS for loading the module (i.e. clibs libraries will use its)
|
|
type - string is enum of values ["windows", "linux", "darwin"] (current OS type by default)
|
|
name - string is a OS type and version (mock OS name by type by default)
|
|
arch - string is enum of values ["386", "amd64"] (current OS/interpreter arch by default)
|
|
__mock.agents - table (list) of tables (dict) are agents list which will be connected to the module state
|
|
the list will be automatically enriched to the local connection from other side (mock side)
|
|
id - string is the agent ID in MD5 hash format (rand ID by default)
|
|
ip - string is the IP address with port which show in connection as a source (127.0.0.1:RAND by default)
|
|
ips - table (list) of strings which each string must be in CIDR format ([ {IP}/24 ] by default)
|
|
gid - string is a group ID of the agent in MD5 hash format (__mock.group_id by default)
|
|
ver - string is a version of the agent binary (v1.0.0.0 by default)
|
|
src - srting is a source token of the agent connection (rand ID by default)
|
|
dst - srting is a destination token of the agent connection (rand ID by default)
|
|
type - string is enum of values ["VXAgent", "Browser", "External"] (VXAgent by default)
|
|
host - string is a hostname of the agent ({ID}.local by default)
|
|
os_type - string is enum of values ["windows", "linux", "darwin"] (windows by default)
|
|
os_name - string is a OS type and version ("Microsoft Windows 10.0" by default)
|
|
os_arch - string is enum of values ["386", "amd64"] (amd64 by default)
|
|
__mock.modules - table (list) of tables (dict) are modules list which will be registered into imc for the api
|
|
the list will be automatically enriched to the current module state
|
|
name - string is a module name without spaces (it's required key)
|
|
gid - string is a group ID of the module in MD5 hash format (__mock.group_id by default)
|
|
token - string is imc token for the module (preferably not to use and will use rand imc token)
|
|
__mock.routes - table (list) of tables (dict) are routes list which will be registered into routes for the api
|
|
src - srting is a source token of the agent connection (it's required key)
|
|
dst - srting is a destination token of the agent connection (it's required key)
|
|
__mock.policy_id - string is the current policy ID in MD5 hash format (rand ID by default)
|
|
__mock.group_id - string is the current group ID in MD5 hash format (rand ID by default)
|
|
__mock.agent_id - string is the current agent ID in MD5 hash format (rand ID by default)
|
|
__mock.agent_conn - table (dict) is a connection structure from mock side as the same of __mock.agents[1] struct
|
|
__mock.server_conn - table (dict) is a structure of connection to the server for mocking on the agent side
|
|
scheme - string is enum of values ["ws", "wss"] (wss by default)
|
|
host - string is the IP address or domain which show in connection as a destination (server.local by default)
|
|
port - string is the port number which show in connection as a destination (8443 by default)
|
|
ips - table (list) of strings are IP addresses to which resolve server host ([127.0.0.1] by default)
|
|
__mock.sec - table (dict) is the secure storage state key-value which would was loaded from policy config
|
|
---------------------------------------------------
|
|
-- auto generated keys
|
|
---------------------------------------------------
|
|
__mock.module_info - table (dict) is a combination for info.json file and module item from config.json file
|
|
__mock.module_type - string is a reference to __mock.side: "smodule" or "cmodule"; res path: "module/version/type"
|
|
__mock.module_path - string is the path to "module/version" folder
|
|
__mock.module_conf_path - string is the path to "module/version/config" folder; folder contains *.json files
|
|
__mock.module_code_path - string is the path to "module/version/type" folder; end of dir is "smodule" or "cmodule"
|
|
__mock.mock_token - string is a vxproto token which identify other side (mock side)
|
|
__mock.module_token - string is a vxproto token which identify module side
|
|
__mock.module_imc_token - string is a imc token of the module which builded from __mock.module and __mock.group_id
|
|
__mock.groups - table (list) of group hashes which builded from __mock.modules list and their gid field
|
|
__mock.trace - function is a tracing method to control using of MOCK API from the module code
|
|
__mock.args - table (dict) is a reflection of the args.json file and parse it from json file
|
|
__mock.cbs - table (dict) is a map to store internal callbacks from module to send vxproto packets
|
|
__mock.is_closed - boolean is a flag of stopping the module to pass it the module via api
|
|
---------------------------------------------------
|
|
-- private methods or validators after initialize
|
|
---------------------------------------------------
|
|
__mock.rand_hash()
|
|
__mock.rand_imc_token()
|
|
__mock.rand_token()
|
|
__mock.rand_uuid()
|
|
__mock.make_imc_token(mname, gid)
|
|
__mock.check_hash(hash)
|
|
__mock.check_imc_token(imc_token)
|
|
__mock.check_token(token)
|
|
__mock.check_domain(domain)
|
|
__mock.check_ipv4(ip)
|
|
__mock.check_ipv4_with_port(tuple)
|
|
__mock.check_port(sport)
|
|
__mock.check_cidr(cidr)
|
|
__mock.test(name, func) - wrapper function for coroutine starting
|
|
__mock.tmppath(name) - function that generates full path of file named by "name" within tmp directory
|
|
---------------------------------------------------
|
|
-- public methods after initialize; use in self too
|
|
---------------------------------------------------
|
|
__mock:expect(etype, filter)
|
|
__mock:add_context(etype, data)
|
|
__mock:pop_from_context(etype, filter)
|
|
__mock:module_start()
|
|
__mock:module_stop()
|
|
__mock:module_update_config(conf, act_conf, ev_conf)
|
|
__mock:disconnect()
|
|
__mock:connect()
|
|
__mock:send_data(src, dst, data)
|
|
__mock:send_file(src, dst, path, name)
|
|
__mock:send_text(src, dst, text, name)
|
|
__mock:send_msg(src, dst, msg, mtype)
|
|
__mock:send_action(src, dst, data, name)
|
|
--]===============================================]
|
|
|
|
local ffi = require("ffi")
|
|
local lfs = require("lfs")
|
|
local md5 = require("md5")
|
|
local glue = require("glue")
|
|
local crc32 = require("crc32")
|
|
local cjson = require("cjson.safe")
|
|
local luapath = require("path")
|
|
math.randomseed(crc32(tostring({})))
|
|
|
|
local function combine_path(...)
|
|
local t = glue.pack(...)
|
|
local path = t[1] or ""
|
|
for i = 2, #t do
|
|
path = luapath.combine(path, t[i])
|
|
end
|
|
return path
|
|
end
|
|
|
|
local function check_file(path)
|
|
return lfs.attributes(path, "mode") == "file"
|
|
end
|
|
|
|
local function check_dir(path)
|
|
return lfs.attributes(path, "mode") == "directory"
|
|
end
|
|
|
|
local check_lua_files_in_dir
|
|
check_lua_files_in_dir = function(dir)
|
|
for file in lfs.dir(dir) do
|
|
local result
|
|
local file_path = combine_path(dir, file)
|
|
if not glue.indexof(file, { ".", "..", ".gitkeep" }) then
|
|
if lfs.attributes(file_path, "mode") == "file" then
|
|
result = luapath.ext(file) == "lua"
|
|
elseif lfs.attributes(file_path, "mode") == "directory" then
|
|
result = check_lua_files_in_dir(file_path)
|
|
end
|
|
end
|
|
if result then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local clean_dir
|
|
clean_dir = function(dir)
|
|
for file in lfs.dir(dir) do
|
|
local file_path = combine_path(dir, file)
|
|
if not glue.indexof(file, { ".", "..", ".gitkeep" }) then
|
|
if lfs.attributes(file_path, "mode") == "file" then
|
|
os.remove(file_path)
|
|
elseif lfs.attributes(file_path, "mode") == "directory" then
|
|
clean_dir(file_path)
|
|
lfs.rmdir(file_path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---------------------------------------------------
|
|
-- MOCK chack general __mock object to use API
|
|
---------------------------------------------------
|
|
assert(type(__mock) == "table", "__mock must be initialized")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
__mock.is_closed = false
|
|
__mock.vars = __mock.vars or {}
|
|
assert(type(__mock.vars) == "table", "__mock.vars must be table type")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
__mock.cbs = {}
|
|
__mock.is_closed = false
|
|
|
|
__mock.stage = __mock.stage or {}
|
|
assert(type(__mock.stage) == "table", "__mock.stage must be table type")
|
|
__mock.stage.time = __mock.stage.time or os.time()
|
|
assert(type(__mock.stage.time) == "number", "__mock.stage.time must be number type")
|
|
__mock.stage.ctx = __mock.stage.ctx or {}
|
|
assert(type(__mock.stage.ctx) == "table", "__mock.stage.ctx must be table type")
|
|
|
|
__mock.timeout = __mock.timeout or 60
|
|
assert(type(__mock.timeout) == "number", "__mock.timeout must be number type")
|
|
assert(__mock.timeout >= 0 and __mock.timeout <= 3600, "__mock.timeout must be [0, 3600]")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
assert(type(__mock.module) == "string", "__mock.module must be string type")
|
|
assert(type(__mock.version) == "string", "__mock.version must be string type")
|
|
|
|
assert(__mock.side == "server" or __mock.side == "agent",
|
|
"__mock.side must be server or agent value")
|
|
__mock.module_type = __mock.side == "server" and "smodule" or "cmodule"
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
__mock.tmpdir = __mock.tmpdir or combine_path(lfs.currentdir(), "tmpdir")
|
|
assert(type(__mock.tmpdir) == "string", "__mock.tmpdir must be string type")
|
|
lfs.mkdir(__mock.tmpdir)
|
|
assert(check_dir(__mock.tmpdir), "__mock.tmpdir must be exist in FS")
|
|
assert(not check_dir(combine_path(__mock.tmpdir, __mock.module)),
|
|
"__mock.tmpdir must not contains folder with module name")
|
|
assert(not check_file(combine_path(__mock.tmpdir, "config.json")),
|
|
"__mock.tmpdir must not contains config.json file")
|
|
assert(not check_lua_files_in_dir(__mock.tmpdir),
|
|
"__mock.tmpdir must not contains *.lua files inside")
|
|
clean_dir(__mock.tmpdir)
|
|
lfs.mkdir(combine_path(__mock.tmpdir, "data"))
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK build module paths to key folders
|
|
---------------------------------------------------
|
|
__mock.base_path = __mock.base_path or lfs.currentdir()
|
|
assert(type(__mock.base_path) == "string", "__mock.base_path must be string type")
|
|
assert(check_dir(__mock.base_path), "__mock.base_path must be exist in FS")
|
|
assert(check_file(combine_path(__mock.base_path, "config.json")),
|
|
"__mock.base_path must contains config.json file")
|
|
|
|
__mock.module_path = combine_path(__mock.base_path, __mock.module, __mock.version)
|
|
assert(check_dir(__mock.module_path), "__mock.module_path must be exist in FS: " .. __mock.module_path)
|
|
|
|
__mock.module_conf_path = combine_path(__mock.module_path, "config")
|
|
assert(check_dir(__mock.module_conf_path), "__mock.module_conf_path must be exist in FS: " .. __mock.module_conf_path)
|
|
|
|
__mock.module_code_path = combine_path(__mock.module_path, __mock.module_type)
|
|
assert(check_dir(__mock.module_code_path), "__mock.module_code_path must be exist in FS: " .. __mock.module_code_path)
|
|
|
|
__mock.tmppath = function(...) return combine_path(__mock.tmpdir, ...) end
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK change current directory to tmp folder
|
|
---------------------------------------------------
|
|
__mock.initial_cwd = lfs.currentdir()
|
|
__mock.cwd = __mock.cwd or combine_path(__mock.initial_cwd, "tmpcwd")
|
|
assert(type(__mock.cwd) == "string", "__mock.cwd must be string type")
|
|
lfs.mkdir(__mock.cwd)
|
|
lfs.mkdir(combine_path(__mock.cwd, "data"))
|
|
lfs.chdir(__mock.cwd)
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK logging settings
|
|
---------------------------------------------------
|
|
__mock.log_level = __mock.log_level or os.getenv("LOG_LEVEL") or "error"
|
|
assert(glue.indexof(__mock.log_level, { "error", "warn", "info", "debug", "trace" }),
|
|
"__mock.log_level must be in [error, warn, info, debug, trace]")
|
|
|
|
__mock.trace = function(func_name, ...) -- return nil
|
|
assert(type(func_name) == "string", "trace function name unknown")
|
|
if __mock.log_level == "trace" then
|
|
print(string.format("[TRACE] mock function '%s'", func_name), ...)
|
|
end
|
|
if type(__mock.callbacks.trace) == "function" then
|
|
__mock.callbacks.trace(func_name, ...)
|
|
end
|
|
end
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
|
|
local function repl(value)
|
|
return string.gsub(value, "[0123456789abcdef]", "x")
|
|
end
|
|
|
|
local function rand(template)
|
|
return string.gsub(template, "[xy]", function(c)
|
|
local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb)
|
|
return string.format("%x", v)
|
|
end)
|
|
end
|
|
|
|
__mock.rand_hash = function()
|
|
return rand("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
|
end
|
|
|
|
__mock.rand_imc_token = function()
|
|
return rand("ffffffffxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
|
end
|
|
|
|
__mock.rand_token = function()
|
|
return rand("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
|
end
|
|
|
|
__mock.rand_uuid = function()
|
|
return rand("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx")
|
|
end
|
|
|
|
__mock.make_imc_token = function(mname, gid)
|
|
assert(type(mname) == "string", "module name must be string type")
|
|
assert(type(gid) == "string", "module group ID must be string type")
|
|
local salt = "thisisimcsaltexamplefortest"
|
|
local data = string.format("%s:%s:%s", gid, mname, salt)
|
|
return string.format("ffffffff%s", glue.tohex(md5.sum(data)))
|
|
end
|
|
|
|
__mock.check_hash = function(hash)
|
|
if type(hash) ~= "string" then return false end
|
|
return repl(hash) == "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
end
|
|
|
|
__mock.check_imc_token = function(imc_token)
|
|
if type(imc_token) ~= "string" then return false end
|
|
if not string.find(imc_token, "^ffffffff") then return false end
|
|
return repl(imc_token) == "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
end
|
|
|
|
__mock.check_token = function(token)
|
|
if type(token) ~= "string" then return false end
|
|
return repl(token) == "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
end
|
|
|
|
---------------------------------------------------
|
|
__mock.policy_id = __mock.policy_id or __mock.rand_hash()
|
|
assert(__mock.check_hash(__mock.policy_id), "__mock.policy_id must be hash format")
|
|
__mock.group_id = __mock.group_id or __mock.rand_hash()
|
|
assert(__mock.check_hash(__mock.group_id), "__mock.group_id must be hash format")
|
|
__mock.agent_id = __mock.agent_id or __mock.rand_hash()
|
|
assert(__mock.check_hash(__mock.agent_id), "__mock.agent_id must be hash format")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
__mock.mock_token = __mock.rand_token()
|
|
__mock.module_token = __mock.rand_token()
|
|
__mock.module_imc_token = __mock.make_imc_token(__mock.module, __mock.group_id)
|
|
__mock.module_callbacks = {}
|
|
---------------------------------------------------
|
|
|
|
local function check_os_type(os_type)
|
|
if type(os_type) ~= "string" then return false end
|
|
return glue.indexof(os_type, { "windows", "linux", "darwin" }) ~= nil
|
|
end
|
|
|
|
local function check_os_name(os_name)
|
|
if type(os_name) ~= "string" then return false end
|
|
return os_name ~= ""
|
|
end
|
|
|
|
local function check_os_arch(os_arch)
|
|
if type(os_arch) ~= "string" then return false end
|
|
return glue.indexof(os_arch, { "386", "amd64" }) ~= nil
|
|
end
|
|
|
|
local function get_os_name_by_type(os_type)
|
|
assert(check_os_type(os_type), "os_type must be valid")
|
|
return ({
|
|
windows = "Microsoft Windows 10.0",
|
|
linux = "Ubuntu 20.04",
|
|
darwin = "macOS 12.4",
|
|
})[os_type]
|
|
end
|
|
|
|
---------------------------------------------------
|
|
__mock.os = __mock.os or {}
|
|
assert(type(__mock.os) == "table", "__mock.os must be table type")
|
|
__mock.os.type = __mock.os.type or ({
|
|
Windows = "windows",
|
|
Linux = "linux",
|
|
OSX = "darwin",
|
|
})[ffi.os]
|
|
assert(check_os_type(__mock.os.type), "__mock.os.type must be in [windows, linux, darwin]")
|
|
__mock.os.name = __mock.os.name or get_os_name_by_type(__mock.os.type)
|
|
assert(check_os_name(__mock.os.name), "__mock.os.name must be valid string")
|
|
__mock.os.arch = __mock.os.arch or ({
|
|
x86 = "386",
|
|
x64 = "amd64",
|
|
})[ffi.arch]
|
|
assert(check_os_arch(__mock.os.arch), "__mock.os.arch must be in [386, amd64]")
|
|
---------------------------------------------------
|
|
|
|
__mock.check_domain = function(domain)
|
|
if type(domain) ~= "string" then return false end
|
|
return domain:match("^(%a%w+)%.?(%w+)$") ~= nil
|
|
end
|
|
|
|
__mock.check_ipv4 = function(ip)
|
|
if type(ip) ~= "string" then return false end
|
|
return ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") ~= nil
|
|
end
|
|
|
|
__mock.check_ipv4_with_port = function(tuple)
|
|
if type(tuple) ~= "string" then return false end
|
|
if tuple:match("^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$") == nil then return false end
|
|
local port = tonumber(glue.collect(glue.gsplit(tuple, ":"))[2])
|
|
return type(port) == "number" and port > 0 and port < 65536
|
|
end
|
|
|
|
__mock.check_port = function(sport)
|
|
if type(sport) ~= "string" then return false end
|
|
if sport:match("^(%d+)$") == nil then return false end
|
|
local port = tonumber(sport)
|
|
return type(port) == "number" and port > 0 and port < 65536
|
|
end
|
|
|
|
__mock.check_cidr = function(cidr)
|
|
if type(cidr) ~= "string" then return false end
|
|
return cidr:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)/(%d+)$") ~= nil
|
|
end
|
|
|
|
local function check_agent_version(aver)
|
|
if type(aver) ~= "string" then return false end
|
|
return aver:match("^v(%d+)%.(%d+)%.(%d+)%.(%d+)") ~= nil
|
|
end
|
|
|
|
local function check_agent_type(atype)
|
|
if type(atype) ~= "string" then return false end
|
|
return glue.indexof(atype, { "VXAgent", "Browser", "External" }) ~= nil
|
|
end
|
|
|
|
local function check_agent_host(ahost)
|
|
if type(ahost) ~= "string" then return false end
|
|
return ahost ~= ""
|
|
end
|
|
|
|
local function check_agent_conn(conn)
|
|
assert(type(conn) == "table", "agent conn must be table type")
|
|
assert(__mock.check_hash(conn.id), "agent conn id must be hash format")
|
|
assert(__mock.check_ipv4_with_port(conn.ip), "agent conn ip must be IPv4 with port format")
|
|
assert(type(conn.ips) == "table", "agent conn ips must be table type")
|
|
assert(glue.indexof(false, glue.map(conn.ips, function(_, ip)
|
|
return __mock.check_cidr(ip)
|
|
end)) == nil, "agent conn ips each record must be CIDR format")
|
|
assert(__mock.check_hash(conn.gid), "agent conn group id must be hash format")
|
|
assert(check_agent_version(conn.ver), "agent conn version must be semver format")
|
|
assert(__mock.check_token(conn.src), "agent conn src must be vxproto token format")
|
|
assert(__mock.check_token(conn.dst), "agent conn dst must be vxproto token format")
|
|
assert(check_agent_type(conn.type), "agent conn type must be in [VXAgent, Browser, External]")
|
|
assert(check_agent_host(conn.host), "agent conn host name must be valid format")
|
|
assert(check_os_type(conn.os_type), "agent conn os type must be in [windows, linux, darwin]")
|
|
assert(check_os_name(conn.os_name), "agent conn os name must be valid string")
|
|
assert(check_os_arch(conn.os_arch), "agent conn os arch must be in [386, amd64]")
|
|
return true
|
|
end
|
|
|
|
---------------------------------------------------
|
|
__mock.agent_conn = __mock.agent_conn or {}
|
|
assert(type(__mock.agent_conn) == "table", "__mock.agent_conn must be table type")
|
|
__mock.agent_conn = {
|
|
id = __mock.agent_conn.id or __mock.agent_id,
|
|
ip = __mock.agent_conn.ip or string.format("127.0.0.1:%d", math.random(32768, 65535)),
|
|
ips = __mock.agent_conn.ips or { "127.0.0.1/8" },
|
|
gid = __mock.agent_conn.gid or __mock.group_id,
|
|
ver = __mock.agent_conn.ver or "v1.0.0.0",
|
|
src = __mock.agent_conn.src or __mock.module_token,
|
|
dst = __mock.agent_conn.dst or __mock.mock_token,
|
|
type = __mock.agent_conn.type or "VXAgent",
|
|
host = __mock.agent_conn.host or "mock.local",
|
|
os_type = __mock.agent_conn.os_type or "windows",
|
|
os_name = __mock.agent_conn.os_name or get_os_name_by_type(__mock.agent_conn.os_type or "windows"),
|
|
os_arch = __mock.agent_conn.os_arch or "amd64",
|
|
}
|
|
assert(check_agent_conn(__mock.agent_conn), "__mock.agent_conn must be valid conn structure")
|
|
---------------------------------------------------
|
|
|
|
local function check_server_conn(conn)
|
|
assert(type(conn) == "table", "server conn must be table type")
|
|
assert(glue.indexof(conn.scheme, { "ws", "wss" }),
|
|
"server conn scheme must be in [ws, wss]")
|
|
assert(__mock.check_ipv4(conn.host) or __mock.check_domain(conn.host),
|
|
"server conn host must be IPv4 or Domain format")
|
|
assert(__mock.check_port(conn.port), "server conn port must be number format")
|
|
assert(type(conn.ips) == "table", "server conn ips must be table type")
|
|
assert(glue.indexof(false, glue.map(conn.ips, function(_, ip)
|
|
return __mock.check_ipv4(ip)
|
|
end)) == nil, "server conn ips each record must be IPv4 format")
|
|
return true
|
|
end
|
|
|
|
---------------------------------------------------
|
|
__mock.server_conn = __mock.server_conn or {}
|
|
assert(type(__mock.server_conn) == "table", "__mock.server_conn must be table type")
|
|
__mock.server_conn = {
|
|
scheme = __mock.server_conn.scheme or "wss",
|
|
host = __mock.server_conn.host or "server.local",
|
|
port = __mock.server_conn.port or "8443",
|
|
ips = __mock.server_conn.ips or { "127.0.0.1" },
|
|
}
|
|
assert(check_server_conn(__mock.server_conn), "__mock.server_conn must be valid conn structure")
|
|
---------------------------------------------------
|
|
|
|
local function patch_agent_conn(conn)
|
|
assert(type(conn) == "table", "agent conn must be table type")
|
|
local id = conn.id or __mock.rand_hash()
|
|
local ip = conn.ip or string.format("127.0.0.1:%d", math.random(32768, 65535))
|
|
local os_type = get_os_name_by_type(conn.os_type or "windows")
|
|
return {
|
|
id = id,
|
|
ip = ip,
|
|
ips = conn.ips or { string.format("%s/8", glue.gsplit(tostring(ip) or "127.0.0.1", ":")()) },
|
|
gid = conn.gid or __mock.group_id,
|
|
ver = conn.ver or "v1.0.0.0",
|
|
src = conn.src or __mock.rand_token(),
|
|
dst = conn.dst or __mock.rand_token(),
|
|
type = conn.type or "VXAgent",
|
|
host = conn.host or string.format("%s.local", id),
|
|
os_type = conn.os_type or "windows",
|
|
os_name = conn.os_name or os_type,
|
|
os_arch = conn.os_arch or "amd64",
|
|
}
|
|
end
|
|
|
|
local function check_agent_route(route)
|
|
assert(type(route) == "table", "agent route must be table type")
|
|
assert(__mock.check_token(route.src), "agent route src must be vxproto token format")
|
|
assert(__mock.check_token(route.dst), "agent route dst must be vxproto token format")
|
|
return true
|
|
end
|
|
|
|
local function check_module_name(mname)
|
|
if type(mname) ~= "string" then return false end
|
|
return mname ~= ""
|
|
end
|
|
|
|
local function check_module(module)
|
|
assert(type(module) == "table", "module struct must be table type")
|
|
assert(check_module_name(module.name), "module name must be valid string")
|
|
assert(__mock.check_hash(module.gid), "module group id must be hash format")
|
|
assert(__mock.check_imc_token(module.token), "module token must be imc token format")
|
|
return true
|
|
end
|
|
|
|
local function patch_module(module)
|
|
assert(type(module) == "table", "module struct must be table type")
|
|
local name = module.name
|
|
local gid = module.gid or __mock.group_id
|
|
return {
|
|
name = name,
|
|
gid = gid,
|
|
token = __mock.make_imc_token(name, gid),
|
|
}
|
|
end
|
|
|
|
---------------------------------------------------
|
|
__mock.agents = __mock.agents or {}
|
|
assert(type(__mock.agents) == "table", "__mock.agents must be table type")
|
|
__mock.agents = glue.map(__mock.agents, function(_, conn)
|
|
return patch_agent_conn(conn)
|
|
end)
|
|
assert(glue.indexof(false, glue.map(__mock.agents, function(_, conn)
|
|
return check_agent_conn(conn)
|
|
end)) == nil, "__mock.agents each record must be valid conn structure")
|
|
table.insert(__mock.agents, 1, __mock.agent_conn)
|
|
|
|
__mock.routes = __mock.routes or {}
|
|
assert(type(__mock.routes) == "table", "__mock.routes must be table type")
|
|
assert(glue.indexof(false, glue.map(__mock.routes, function(_, route)
|
|
return check_agent_route(route)
|
|
end)) == nil, "__mock.routes each record must be valid route structure")
|
|
|
|
__mock.modules = __mock.modules or {}
|
|
assert(type(__mock.modules) == "table", "__mock.modules must be table type")
|
|
__mock.modules = glue.map(__mock.modules, function(_, module)
|
|
return patch_module(module)
|
|
end)
|
|
assert(glue.indexof(false, glue.map(__mock.modules, function(_, module)
|
|
return check_module(module)
|
|
end)) == nil, "__mock.modules each record must be valid module structure")
|
|
table.insert(__mock.modules, 1, {
|
|
name = __mock.module,
|
|
gid = __mock.group_id,
|
|
token = __mock.module_imc_token,
|
|
})
|
|
|
|
local groups = {}
|
|
glue.map(__mock.modules, function(_, k)
|
|
groups[k.gid] = true
|
|
end)
|
|
__mock.groups = {}
|
|
glue.map(groups, function(tk)
|
|
table.insert(__mock.groups, tk)
|
|
end)
|
|
---------------------------------------------------
|
|
|
|
local function copy_file(src, dst, force)
|
|
local f, err = io.open(src, 'rb')
|
|
if not f then return nil, err end
|
|
|
|
local t, ok
|
|
if not force then
|
|
t = io.open(dst, 'rb')
|
|
if t then
|
|
f:close()
|
|
t:close()
|
|
return nil, "file alredy exists"
|
|
end
|
|
end
|
|
|
|
t, err = io.open(dst, 'w+b')
|
|
if not t then
|
|
f:close()
|
|
return nil, err
|
|
end
|
|
|
|
local CHUNK_SIZE = 4096
|
|
while true do
|
|
local chunk = f:read(CHUNK_SIZE)
|
|
if not chunk then break end
|
|
ok, err = t:write(chunk)
|
|
if not ok then
|
|
t:close()
|
|
f:close()
|
|
return nil, err or "can not write"
|
|
end
|
|
end
|
|
|
|
t:close()
|
|
f:close()
|
|
collectgarbage("collect")
|
|
return true
|
|
end
|
|
|
|
local copy_dir
|
|
copy_dir = function(base, src, dst)
|
|
if lfs.attributes(src, "mode") ~= "directory" then return end
|
|
for file in lfs.dir(src) do
|
|
local file_path = combine_path(src, file)
|
|
if not glue.indexof(file, { ".", ".." }) then
|
|
if lfs.attributes(file_path, "mode") == "file" then
|
|
copy_file(combine_path(src, file), combine_path(dst, file), true)
|
|
elseif lfs.attributes(file_path, "mode") == "directory" then
|
|
local base_dir = combine_path(base, file)
|
|
local src_dir = combine_path(src, file)
|
|
local dst_dir = combine_path(dst, file)
|
|
lfs.mkdir(dst_dir)
|
|
copy_dir(base_dir, src_dir, dst_dir)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function read_file(file_path)
|
|
local file_data
|
|
local fhandle = io.open(file_path, "rb")
|
|
if nil ~= fhandle then
|
|
file_data = fhandle:read("*all")
|
|
fhandle:close()
|
|
end
|
|
return file_data
|
|
end
|
|
|
|
local read_dir
|
|
read_dir = function(files, base, dir)
|
|
for file in lfs.dir(dir) do
|
|
local file_path = combine_path(dir, file)
|
|
if not glue.indexof(file, { ".", "..", "data", "clibs" }) then
|
|
if lfs.attributes(file_path, "mode") == "file" then
|
|
files[combine_path(base, file)] = read_file(file_path)
|
|
elseif lfs.attributes(file_path, "mode") == "directory" then
|
|
read_dir(files, combine_path(base, file), file_path)
|
|
end
|
|
end
|
|
end
|
|
return files
|
|
end
|
|
|
|
---------------------------------------------------
|
|
-- MOCK load files into memory and copy it into tmp
|
|
---------------------------------------------------
|
|
__mock.files = read_dir({}, "", __mock.module_code_path)
|
|
copy_dir("", combine_path(__mock.module_code_path, "data"), combine_path(__mock.tmpdir, "data"))
|
|
local clibs = combine_path(__mock.module_code_path, "clibs", __mock.os.type, __mock.os.arch)
|
|
copy_dir("", clibs, __mock.tmpdir)
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK load config files into memory as a strings
|
|
---------------------------------------------------
|
|
local config = read_dir({}, "", __mock.module_conf_path)
|
|
__mock.config = {}
|
|
glue.map(config, function(name, conf)
|
|
__mock.config[glue.gsplit(name, "%.")()] = conf
|
|
end)
|
|
|
|
__mock.module_info = cjson.decode(__mock.info) or {}
|
|
__mock.module_info.tags = nil
|
|
__mock.module_info.system = nil
|
|
__mock.module_info.name = __mock.module
|
|
__mock.module_info.version = __mock.version
|
|
__mock.module_info.group_id = __mock.group_id
|
|
__mock.module_info.policy_id = __mock.policy_id
|
|
local config_json = cjson.decode(read_file(combine_path(__mock.base_path, "config.json")))
|
|
glue.map(config_json, function(_, mod)
|
|
local name = mod.name
|
|
local version = string.format("%d.%d.%d", mod.version.major, mod.version.minor, mod.version.patch)
|
|
if name == __mock.module and version == __mock.version then
|
|
glue.merge(__mock.module_info, mod)
|
|
end
|
|
end)
|
|
local current_date = os.date("%Y-%m-%d %H:%M:%S", os.time())
|
|
glue.merge(__mock.module_info, {
|
|
state = "draft",
|
|
last_module_update = current_date,
|
|
last_update = current_date,
|
|
})
|
|
__mock.config.module_info = cjson.encode(__mock.module_info)
|
|
|
|
__mock.sec = __mock.sec or {}
|
|
assert(type(__mock.sec) == "table", "__mock.sec must be table type")
|
|
assert(glue.indexof(false, glue.map(__mock.sec, function(tk)
|
|
return type(tk) == "string"
|
|
end)) == nil, "__mock.sec each record key must be string type")
|
|
assert(glue.indexof(false, glue.map(__mock.sec, function(_, tv)
|
|
return type(tv) == "string"
|
|
end)) == nil, "__mock.sec each record value must be string type")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK load and parse args.json file into memory
|
|
---------------------------------------------------
|
|
local args_file_path = combine_path(__mock.module_code_path, "args.json")
|
|
local args_file_data = read_file(args_file_path)
|
|
assert(type(args_file_data) == "string", "args.json file must be exist")
|
|
local args_file_json = cjson.decode(args_file_data)
|
|
assert(type(args_file_json) == "table", "args.json file must be JSON format")
|
|
for k, v in pairs(args_file_json) do
|
|
assert(type(k) == "string", "args.json root object must contain string keys")
|
|
assert(type(v) == "table", "args.json root object must contain table values")
|
|
for i, vv in ipairs(v) do
|
|
assert(type(i) == "number" and type(vv) == "string", "args.json values must contain array of strings")
|
|
end
|
|
end
|
|
__mock.args = args_file_json
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK setup package path to load module libraries
|
|
---------------------------------------------------
|
|
__mock.initial_path = package.path
|
|
package.path = table.concat({
|
|
package.path,
|
|
combine_path(__mock.module_code_path, "?.lua"),
|
|
combine_path(__mock.module_code_path, "?", "init.lua"),
|
|
}, ";")
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK user code section and vars to test module
|
|
---------------------------------------------------
|
|
local function require_module()
|
|
local module_main = 'main'
|
|
local module_name = __mock.module .. '.' .. __mock.version
|
|
|
|
local searchers, loaded = package.searchers, package.loaded
|
|
local module = loaded[module_name]
|
|
if module then
|
|
return module
|
|
end
|
|
|
|
local msg = {}
|
|
local loader, param
|
|
for _, searcher in ipairs(searchers) do
|
|
loader, param = searcher(module_main)
|
|
if type(loader) == "function" then
|
|
break
|
|
end
|
|
if type(loader) == "string" then
|
|
-- `loader` is actually an error message
|
|
msg[#msg + 1] = loader
|
|
end
|
|
loader = nil
|
|
end
|
|
if loader == nil then
|
|
local error_message = ("couldn't find '%s'.lua of '%s' module: %s"):format(module_main, module_name,
|
|
table.concat(msg))
|
|
error(error_message, 2)
|
|
end
|
|
local res = loader(module_name, param)
|
|
if res ~= nil then
|
|
loaded[module_name] = res
|
|
elseif not loaded[module_name] then
|
|
loaded[module_name] = true
|
|
end
|
|
|
|
return loaded[module_name]
|
|
end
|
|
|
|
__mock.stage.coro = __mock.stage.coro or coroutine.create(
|
|
function()
|
|
local result = require_module()
|
|
|
|
package.path = __mock.initial_path
|
|
__mock.cwd = __mock.initial_cwd
|
|
lfs.chdir(__mock.cwd)
|
|
|
|
assert("success" == result, "module failed: " .. result)
|
|
end
|
|
)
|
|
__mock.callbacks = __mock.callbacks or {
|
|
data = function(self, dst, src, data)
|
|
self:add_context("data", { dst = dst, src = src, data = cjson.decode(data) or data })
|
|
return true
|
|
end,
|
|
file = function(self, dst, src, path, name)
|
|
self:add_context("file", { dst = dst, src = src, path = path, data = glue.readfile(path), name = name })
|
|
return true
|
|
end,
|
|
text = function(self, dst, src, data, name)
|
|
self:add_context("text", { dst = dst, src = src, data = cjson.decode(data) or data, name = name })
|
|
return true
|
|
end,
|
|
msg = function(self, dst, src, data, mtype)
|
|
self:add_context("msg", { dst = dst, src = src, data = cjson.decode(data) or data, mtype = mtype })
|
|
return true
|
|
end,
|
|
action = function(self, dst, src, data, name)
|
|
self:add_context("action", { dst = dst, src = src, data = cjson.decode(data) or data, name = name })
|
|
return true
|
|
end,
|
|
push_event = function(self, aid, event)
|
|
self:add_context("event", { aid = aid, event = cjson.decode(event) })
|
|
return true
|
|
end,
|
|
}
|
|
assert(type(__mock.callbacks) == "table", "__mock.callbacks must be table type")
|
|
for name, callback in pairs(__mock.callbacks) do
|
|
assert(glue.indexof(name, { "data", "file", "text", "msg", "action", "push_event", "trace" }),
|
|
"__mock.callbacks table key must be in [data, file, text, msg, action, push_event, trace]")
|
|
assert(type(callback) == "function", "__mock.callbacks table value must be function type")
|
|
end
|
|
---------------------------------------------------
|
|
|
|
---------------------------------------------------
|
|
-- MOCK public API which can be using in scenario
|
|
---------------------------------------------------
|
|
__mock.test = function(name, f)
|
|
print(string.rep("-", 50))
|
|
print("-- TEST " .. name)
|
|
print(string.rep("-", 50))
|
|
local scenario_co = coroutine.create(f)
|
|
-- NOTE: coroutine is needed here cause api.send_* calls in the main execution
|
|
-- can lead to await's from module logic that need to be handled
|
|
while coroutine.status(scenario_co) == "suspended" do
|
|
local result, err = coroutine.resume(scenario_co)
|
|
assert(result, err)
|
|
end
|
|
print(string.rep("-", 50))
|
|
print()
|
|
end
|
|
|
|
__mock.module_stop = function(self)
|
|
self.is_closed = true
|
|
while coroutine.status(self.stage.coro) == "suspended" do
|
|
local result, err = coroutine.resume(self.stage.coro, self)
|
|
assert(result, err)
|
|
end
|
|
end
|
|
|
|
__mock.module_start = function(self)
|
|
self.is_closed = false
|
|
if coroutine.status(self.stage.coro) == "suspended" then
|
|
local result, err = coroutine.resume(self.stage.coro, self)
|
|
assert(result, err)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.module_update_config = function(self, conf)
|
|
local _, _ = self, conf
|
|
end
|
|
|
|
__mock.add_context = function(self, etype, data)
|
|
local _ = self
|
|
self.trace("__mock.add_context", etype, data)
|
|
self.stage.ctx[etype] = self.stage.ctx[etype] or {}
|
|
table.insert(self.stage.ctx[etype], data)
|
|
end
|
|
|
|
__mock.pop_from_context = function(self, etype, filter)
|
|
self.trace("__mock.get_from_context", etype)
|
|
if not self.stage.ctx[etype] then return nil end
|
|
|
|
for i, data in ipairs(self.stage.ctx[etype]) do
|
|
local no_errors, is_correct = pcall(filter, data)
|
|
if no_errors and is_correct then
|
|
table.remove(self.stage.ctx[etype], i)
|
|
return data
|
|
end
|
|
end
|
|
end
|
|
|
|
__mock.clear_expectations = function(self)
|
|
self.stage.ctx = {}
|
|
end
|
|
|
|
__mock.expect = function(self, etype, filter)
|
|
self.trace("__mock.expect", etype)
|
|
local start_time = os.time()
|
|
while true do
|
|
local obj = self:pop_from_context(etype, filter)
|
|
if obj ~= nil then
|
|
self.stage.time = os.time()
|
|
return true
|
|
end
|
|
|
|
local elapsed_time = os.difftime(os.time(), self.stage.time)
|
|
local check_elapsed_time = os.difftime(os.time(), start_time)
|
|
-- TODO: need to have global timeout but also check local for each expectation waiting period
|
|
if self.timeout ~= 0 and elapsed_time >= self.timeout then
|
|
return false, "expectation timed out after " .. check_elapsed_time .. " seconds"
|
|
end
|
|
if coroutine.status(self.stage.coro) == "dead" then
|
|
return false, "coroutine is dead"
|
|
end
|
|
local status, _ = coroutine.resume(self.stage.coro, self)
|
|
if not status then
|
|
return false, "failed to resume coroutine"
|
|
end
|
|
end
|
|
end
|
|
|
|
__mock.disconnect = function(self)
|
|
local _ = self
|
|
end
|
|
|
|
__mock.connect = function(self)
|
|
local _ = self
|
|
end
|
|
|
|
__mock.send_data = function(self, src, dst, data)
|
|
local _ = dst
|
|
if self.module_callbacks.data ~= nil then
|
|
return self.module_callbacks.data(src, data)
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.send_file = function(self, src, dst, path, name)
|
|
local _ = dst
|
|
if self.module_callbacks.file ~= nil then
|
|
return self.module_callbacks.file(src, path, name)
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.send_text = function(self, src, dst, text, name)
|
|
local _ = dst
|
|
if self.module_callbacks.text ~= nil then
|
|
return self.module_callbacks.text(src, text, name)
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.send_msg = function(self, src, dst, msg, mtype)
|
|
local _ = dst
|
|
if self.module_callbacks.msg ~= nil then
|
|
return self.module_callbacks.msg(src, msg, mtype)
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.send_action = function(self, src, dst, data, name)
|
|
local _ = dst
|
|
if self.module_callbacks.action ~= nil then
|
|
return self.module_callbacks.action(src, data, name)
|
|
end
|
|
return false
|
|
end
|
|
|
|
__mock.send_control = function(self, cmtype, data)
|
|
if self.module_callbacks.control ~= nil then
|
|
return self.module_callbacks.control(cmtype, data)
|
|
end
|
|
return false
|
|
end
|
|
---------------------------------------------------
|