Initial commit of mico-framework refactor. Original monolithic script still exists in /src

This commit is contained in:
wes
2025-06-07 23:59:53 -04:00
parent ed94ee9a92
commit eaa1f5ce53
12 changed files with 2009 additions and 245 deletions
+156
View File
@@ -0,0 +1,156 @@
-- skyweave/handlers/network.lua
local utils = require("skyweave.utils") -- Updated path
local network_handler = {}
-- Original: function handlers.get_network_config(params)
function network_handler.get_network_config(request_context)
local iface = request_context.query.interface or "wan" -- Default to "wan"
-- Ensure utils are available for these calls
local live_dns_list, live_search_domains = utils.get_live_dns_servers_from_resolv(iface)
local live_gateway_ip, live_gateway_iface = utils.get_default_gateway_from_route()
-- get_live_interface_info returns: address, mask, device, leasetime, error_message
local live_wan_ip, live_wan_netmask, ifname, dhcp_lease_time, err_msg_live_info = utils.get_live_interface_info(iface)
-- Handle potential error from get_live_interface_info if needed, e.g. log err_msg_live_info
local response_data = { errCode = 0, errMsg = "OK"}
response_data.wwan = {
ssid = utils.uci_get("wireless.wwan.ssid") or "",
bssid = utils.uci_get("wireless.wwan.bssid") or "",
encryption = utils.uci_get("wireless.wwan.encryption") or "",
key = utils.uci_get("wireless.wwan.key") or "", -- Consider redacting this for a GET
device = utils.uci_get("wireless.wwan.device") or "",
wan_dns1 = live_dns_list and live_dns_list[1] or "",
wan_dns2 = live_dns_list and live_dns_list[2] or "",
wan_gateway = live_gateway_ip or "",
wan_ip = live_wan_ip or "",
wan_netmask = live_wan_netmask or "",
wan_ifname = ifname or "", -- from get_live_interface_info
wan_iface = iface, -- the logical interface name queried
wan_dhcp_lease_time = dhcp_lease_time or "" -- from get_live_interface_info
}
response_data.lan = {
ipaddr = utils.uci_get("network.lan.ipaddr") or "",
netmask = utils.uci_get("network.lan.netmask") or "",
gateway = utils.uci_get("network.lan.gateway") or "", -- Typically not set for LAN if router
start = utils.uci_get("dhcp.lan.start") or "",
limit = utils.uci_get("dhcp.lan.limit") or "",
leasetime = utils.uci_get("dhcp.lan.leasetime") or "",
domain = utils.uci_get("dhcp.@dnsmasq[0].domain") or "",
ignore = utils.uci_get("dhcp.lan.ignore") or "" -- bool, uci_get returns "0" or "1"
}
return response_data
end
-- Original: function handlers.set_connect_wifi(params) -> params was POST data
function network_handler.set_connect_wifi(request_context)
-- Parameters from POST body
local body = request_context.body
if not body then
return { errCode = -29, errMsg = "Request body is missing or not valid JSON.", configDone = false }
end
local ssid = body.ssid
local bssid = body.bssid -- Optional
local encryption_type = body.encryption or "none"
local key = body.key -- Optional, but required for encrypted types
local sta_device = body.device or "radio0" -- Default device
-- Define UCI configuration sections and interface names (as in original)
local sta_uci_section = "wireless.wwan"
local sta_ifname = "vif-sta0" -- This is the network interface name, not a physical device
-- Validation (as in original)
if not ssid or ssid == "" then
return { errCode = -30, errMsg = "SSID is required.", configDone = false }
end
local supported_encryptions = {
["none"] = true, ["psk"] = true, ["psk2"] = true,
["wep"] = true, ["wep-open"] = true, ["wep-shared"] = true
}
if not supported_encryptions[encryption_type] then
return { errCode = -31, errMsg = "Unsupported encryption type: " .. tostring(encryption_type), configDone = false }
end
if (encryption_type ~= "none" and encryption_type ~= "") and (not key or key == "") then
return { errCode = -32, errMsg = "Key is required for encryption type: " .. encryption_type, configDone = false }
end
local commands = {}
local function add_cmd(cmd)
table.insert(commands, cmd)
end
-- A) Configure Wireless STA Interface
add_cmd("uci set " .. sta_uci_section .. ".disabled=0")
add_cmd("uci set " .. sta_uci_section .. ".mode=sta")
add_cmd("uci set " .. sta_uci_section .. ".ifname='" .. sta_ifname .. "'")
add_cmd("uci set " .. sta_uci_section .. ".device='" .. sta_device .. "'")
add_cmd("uci set " .. sta_uci_section .. ".network=wan") -- Assign this wireless STA to 'wan' logical network
add_cmd("uci set " .. sta_uci_section .. ".ssid='" .. ssid:gsub("'", "'\''") .. "'") -- Sanitize SSID for shell
if bssid and bssid ~= "" then
add_cmd("uci set " .. sta_uci_section .. ".bssid='" .. bssid:gsub("'", "'\''") .. "'") -- Sanitize BSSID
else
add_cmd("uci delete " .. sta_uci_section .. ".bssid")
end
-- B) Configure Wireless Encryption
if encryption_type == "none" then
add_cmd("uci set " .. sta_uci_section .. ".encryption=none")
add_cmd("uci delete " .. sta_uci_section .. ".key")
else -- covers psk, psk2, wep variants
local uci_enc_val = encryption_type
if encryption_type == "wep" then uci_enc_val = "wep-open" end -- Default WEP to open
add_cmd("uci set " .. sta_uci_section .. ".encryption=" .. uci_enc_val)
add_cmd("uci set " .. sta_uci_section .. ".key='" .. (key or ""):gsub("'", "'\''") .. "'") -- Sanitize key
end
-- C) Configure the Network WAN Interface to use DHCP (it's now the STA interface)
add_cmd("uci set network.wan.proto=dhcp")
-- The physical device for network.wan is effectively controlled by wireless.wwan.network='wan'
-- No need to set network.wan.ifname here, as the system handles it.
-- Remove any static IP settings from WAN as it's now DHCP via WiFi STA
add_cmd("uci delete network.wan.ipaddr; uci delete network.wan.netmask; uci delete network.wan.gateway; uci delete network.wan.dns")
add_cmd("uci delete network.wan.ip6addr; uci delete network.wan.ip6gw; uci delete network.wan.ip6prefix")
-- D) Deconflict with LTE Mode (original behavior)
add_cmd("rm -f /etc/lte_mode")
-- E) Commit UCI changes and restart network
add_cmd("uci commit wireless")
add_cmd("uci commit network")
-- The original script had /etc/init.d/network restartall.
-- A more targeted restart might be 'wifi reload && /etc/init.d/network restart wan'
-- or simply '/etc/init.d/network restart'
-- For now, keeping 'restartall' for consistency with original behavior.
add_cmd("/etc/init.d/network restartall")
local command_string = table.concat(commands, "; ")
-- The router will wrap this with (sleep 2; ...) &
local response_msg = "OK (Configuration accepted. Device will attempt to connect to SSID: " .. ssid
if bssid and bssid ~= "" then
response_msg = response_msg .. " | BSSID: " .. bssid .. ")"
else
response_msg = response_msg .. ")"
end
return {
async = true,
command = command_string,
response = {
errCode = 0,
errMsg = response_msg,
configDone = true -- Indicate config was applied, pending connection
}
}
end
return network_handler
+63
View File
@@ -0,0 +1,63 @@
-- skyweave/handlers/system.lua
local utils = require("skyweave.utils") -- Updated path
local system_handler = {}
-- Original function: handlers.get_system_log_parsed(params)
-- Refactored to: system_handler.get_system_log_parsed(request_context)
function system_handler.get_system_log_parsed(request_context)
-- This handler doesn't use specific query parameters or body from request_context for its main logic.
-- It primarily executes 'logread'.
-- utils.execute_command is preferred over io.popen directly for consistency,
-- though the original used io.popen. We'll stick to the original's direct use here
-- as utils.execute_command might not return the handle needed for line-by-line reading
-- if logread output is very large. However, logread is usually read all at once.
-- Let's use utils.execute_command for consistency with other potential commands.
local log_content = utils.execute_command("logread")
if log_content == nil then
-- utils.execute_command might return nil, an empty string, or output.
-- Handle nil case as failure to execute or empty output.
return {
errCode = -20,
errMsg = "Failed to execute logread command or command yielded no output.",
parsed_logs = {},
processing_summary = "Error executing logread."
}
end
local parsed_logs_list = {}
local lines_processed = 0
local lines_failed_parse = 0
if log_content ~= "" then
for line in string.gmatch(log_content, "[^\r\n]+") do -- Match lines not containing CR or LF
if line ~= "" then -- Process non-empty lines
lines_processed = lines_processed + 1
-- Use utils.parse_log_line from the utils module
local parsed_entry = utils.parse_log_line(line)
if parsed_entry.parse_error then
lines_failed_parse = lines_failed_parse + 1
end
table.insert(parsed_logs_list, parsed_entry)
end
end
end
local summary_msg_key = (lines_processed == 0 and log_content == "") and "No log entries found." or
string.format("Processed %d lines. %d lines failed to parse.", lines_processed, lines_failed_parse)
local response_data = {
parsed_logs = parsed_logs_list,
processing_summary = summary_msg_key,
errCode = 0,
errMsg = "OK"
-- The 'configDone = false' from original seems irrelevant here, omitting.
}
return response_data
end
return system_handler
+149
View File
@@ -0,0 +1,149 @@
-- skyweave/handlers/wifi.lua
-- No direct utils needed for the core logic of get_wifi_scan,
-- as it uses io.popen for specific parsing needs.
-- local utils = require("../utils")
local wifi_handler = {}
-- Original function: handlers.get_wifi_scan(params)
-- Refactored to: wifi_handler.get_wifi_scan(request_context)
function wifi_handler.get_wifi_scan(request_context)
local ifname = request_context.query.ifname or "radio0" -- Default to radio0
-- Validate ifname to known radio interfaces to prevent command injection
if not (ifname == "radio0" or ifname == "radio1") then
return {
errCode = -11,
errMsg = "Invalid interface name specified: " .. tostring(ifname),
result_count = 0, -- Consistent with original error structure
results = {}
}
end
local command = "iwinfo " .. ifname .. " scan"
local handle = io.popen(command)
if not handle then
return {
errCode = -12,
errMsg = "Failed to execute iwinfo command for " .. tostring(ifname),
result_count = 0,
results = {}
}
end
local output = handle:read("*a")
handle:close()
if output == nil or output == "" then
return {
errCode = 0,
errMsg = "OK (no scan results or empty output from command)", -- Clarified message
result_count = 0,
results = {}
}
end
local results_list = {}
local current_ap_data = nil
-- The parsing logic below is complex and specific to iwinfo output.
-- It's preserved as closely as possible from the original script.
for line in string.gmatch(output, "[^\r\n]+") do
line = line:match("^%s*(.-)%s*$") -- Trim whitespace
local cell_addr = line:match("^Cell %d+%s*-%s*Address:%s*([:%x%dABCDEFabcdef]+)")
if cell_addr then
if current_ap_data and current_ap_data.address then
current_ap_data.ssid = current_ap_data.ssid or ""
current_ap_data.quality = current_ap_data.quality or 0
current_ap_data.encryption = current_ap_data.encryption or "none"
current_ap_data.signal = current_ap_data.signal or 0
current_ap_data.mode = current_ap_data.mode or ""
current_ap_data.channel = current_ap_data.channel -- Keep as nil if not found
table.insert(results_list, current_ap_data)
end
current_ap_data = { address = cell_addr:upper() }
elseif current_ap_data then
local essid_val = line:match("^ESSID:%s*\"(.*)\"")
if not essid_val then
essid_val = line:match("^ESSID:%s*(.+)")
end
local mode_val, channel_val = line:match("^Mode:%s*(%S+)%s*Channel:%s*(%d+)")
local signal_val_dbm, quality_num_str, quality_max_str = line:match("^Signal:%s*([%-?%d]+) dBm%s*Quality:%s*(%d+)/(%d+)")
local encryption_val = line:match("^Encryption:%s*(.+)")
if essid_val then
current_ap_data.ssid = essid_val:match("^%s*(.-)%s*$")
elseif mode_val then
current_ap_data.mode = mode_val
if channel_val then
current_ap_data.channel = tonumber(channel_val)
end
elseif signal_val_dbm then
current_ap_data.signal = tonumber(signal_val_dbm)
if quality_num_str and quality_max_str then
local q_num = tonumber(quality_num_str)
local q_max = tonumber(quality_max_str)
if q_num and q_max and q_max > 0 then
current_ap_data.quality = math.floor((q_num * 100) / q_max)
elseif q_num then
current_ap_data.quality = q_num
end
end
elseif encryption_val then
current_ap_data.encryption = encryption_val:match("^%s*(.-)%s*$")
end
end
end
if current_ap_data and current_ap_data.address then
current_ap_data.ssid = current_ap_data.ssid or ""
current_ap_data.quality = current_ap_data.quality or 0
current_ap_data.encryption = current_ap_data.encryption or "none"
current_ap_data.signal = current_ap_data.signal or 0
current_ap_data.mode = current_ap_data.mode or ""
current_ap_data.channel = current_ap_data.channel
table.insert(results_list, current_ap_data)
end
for _, ap_data in ipairs(results_list) do
ap_data.ssid = ap_data.ssid or ""
-- address should always be there
ap_data.quality = ap_data.quality or 0
ap_data.encryption = ap_data.encryption or "none"
ap_data.signal = ap_data.signal or 0
ap_data.mode = ap_data.mode or ""
-- channel can be nil, or default it: ap_data.channel = ap_data.channel or 0
end
-- The original script had logic for 'excluded_ssids' and 'unique_excluding_known'.
-- This seems like business logic specific to that list, keeping it.
local excluded_ssids = {
["NittanyMtnKOA"] = true,
["NittanyMTNKOA_EXT2"] = true,
["OmnissiahsReach"] = true
}
local unique_excluding_known = 0
for _, ap_data in ipairs(results_list) do
if not excluded_ssids[ap_data.ssid] then
unique_excluding_known = unique_excluding_known + 1
end
end
return {
unique_BSSIDs = #results_list,
foreign_networks = unique_excluding_known,
results = results_list,
errCode = 0,
errMsg = "OK",
-- 'configDone = false' from original seems less relevant here,
-- but keeping it if it's an expected part of this specific endpoint's contract.
configDone = false
}
end
return wifi_handler
+103
View File
@@ -0,0 +1,103 @@
-- skyweave/request.lua
local json = require("json") -- Assuming json.lua is in a standard Lua path e.g. /usr/lib/lua
-- Removed local parse_query_string function, will use utils.parse_query_string
-- local function parse_query_string(query_string)
-- local params = {}
-- if not query_string or query_string == "" then return params end
--
-- for pair in query_string:gmatch("[^&]+") do
-- local key, value = pair:match("([^=]+)=?(.*)")
-- if key then
-- key = key:gsub("+", " "):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
-- value = value or "" -- Ensure value is a string, even if empty after '='
-- value = value:gsub("+", " "):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
-- params[key] = value
-- end
-- end
-- return params
-- end
local utils = require("skyweave.utils") -- Updated path
local request_module = {}
function request_module.create_request_context()
local request = {}
request.method = os.getenv("REQUEST_METHOD") or "GET" -- Default to GET if not set
local query_string_env = os.getenv("QUERY_STRING") or ""
request.query = utils.parse_query_string(query_string_env)
-- PATH_INFO is the part of the URL after the script name and before the query string.
-- e.g. /cgi-bin/skyweave.lua/some/path?foo=bar -> /some/path
-- This is dependent on Nginx configuration (e.g. using fastcgi_split_path_info).
-- If not properly set by Nginx, it might be nil or empty.
request.path = os.getenv("PATH_INFO") or "/"
if request.path == "" then request.path = "/" end
request.headers = {}
-- Populate headers using os.getenv for specific, known CGI variables
local content_type_env = os.getenv("CONTENT_TYPE") -- Use different var name to avoid conflict with request.headers.content-type
if content_type_env then request.headers["content-type"] = content_type_env end
local user_agent = os.getenv("HTTP_USER_AGENT")
if user_agent then request.headers["user-agent"] = user_agent end
local accept_header = os.getenv("HTTP_ACCEPT")
if accept_header then request.headers["accept"] = accept_header end
-- Add other common headers if needed, for example:
-- local accept_encoding = os.getenv("HTTP_ACCEPT_ENCODING")
-- if accept_encoding then request.headers["accept-encoding"] = accept_encoding end
--
-- local accept_language = os.getenv("HTTP_ACCEPT_LANGUAGE")
-- if accept_language then request.headers["accept-language"] = accept_language end
--
-- local referer = os.getenv("HTTP_REFERER")
-- if referer then request.headers["referer"] = referer end
--
-- Note: CONTENT_LENGTH is retrieved later via os.getenv("CONTENT_LENGTH") specifically for body processing.
-- It is not typically added to this general request.headers table unless other logic specifically needs it there.
request.raw_body = nil
request.body = nil
request.body_parse_error = nil
if request.method == "POST" or request.method == "PUT" or request.method == "PATCH" then
local content_length_str = os.getenv("CONTENT_LENGTH")
local content_length = tonumber(content_length_str or 0)
if content_length and content_length > 0 then
-- Removed: io.stdin:setvbuf("binary") -- Not supported/needed for JSON POST or general text POST
request.raw_body = io.stdin:read(content_length)
-- Try to parse if content type suggests JSON and JSON lib is available
local content_type = request.headers["content-type"] or ""
if json and content_type:lower():match("application/json") then
if request.raw_body and request.raw_body ~= "" then
local success, decoded_or_error = pcall(json.decode, request.raw_body)
if success then
request.body = decoded_or_error
else
request.body_parse_error = "JSON decode error: " .. tostring(decoded_or_error)
end
else
request.body_parse_error = "Empty body received with application/json Content-Type"
end
else
-- If not JSON or no JSON lib, body remains nil, raw_body has the content.
-- Handlers can inspect raw_body if they expect other content types.
if not json and content_type:lower():match("application/json") then
request.body_parse_error = "Received JSON Content-Type but no JSON library is loaded."
end
end
end
end
return request
end
return request_module
+44
View File
@@ -0,0 +1,44 @@
-- skyweave/response.lua
local json = require("json") -- Assuming json.lua is in a standard Lua path e.g. /usr/lib/lua
local response = {}
-- Default HTTP status messages corresponding to codes
local http_status_messages = {
[200] = "200 OK",
[201] = "201 Created",
[204] = "204 No Content",
[400] = "400 Bad Request",
[401] = "401 Unauthorized",
[403] = "403 Forbidden",
[404] = "404 Not Found",
[405] = "405 Method Not Allowed",
[500] = "500 Internal Server Error"
}
function response.json(data_table, http_status_code)
http_status_code = http_status_code or 200
local status_message = http_status_messages[http_status_code] or tostring(http_status_code)
print("Status: " .. status_message)
print("Content-Type: application/json")
print("") -- End of headers
io.stdout:write(json.encode(data_table))
io.stdout:flush()
end
function response.error(error_message, http_status_code, internal_error_code)
http_status_code = http_status_code or 500
internal_error_code = internal_error_code or -1
error_message = error_message or "Unknown error"
local status_message = http_status_messages[http_status_code] or tostring(http_status_code)
print("Status: " .. status_message)
print("Content-Type: application/json")
print("") -- End of headers
io.stdout:write(json.encode({ errCode = internal_error_code, errMsg = error_message }))
io.stdout:flush()
end
return response
+134
View File
@@ -0,0 +1,134 @@
-- skyweave/router.lua
local response = require("skyweave.response") -- For sending 404s, errors etc. (Path Confirmed)
local Router = {}
Router.__index = Router
function Router:new()
local instance = {
routes = {} -- Format: { ["GET:/path"] = handler_func }
}
setmetatable(instance, Router)
return instance
end
function Router:add_route(method, path_key, handler_func)
if not method or not path_key or not handler_func then
-- Handle error: log or throw, but for now, just print to stderr for debugging
io.stderr:write("Error: method, path_key, and handler_func are required to add a route.\n")
return
end
local key = method:upper() .. ":" .. path_key
self.routes[key] = handler_func
-- io.stderr:write("Route added: " .. key .. "\n") -- For debugging
end
-- This function translates the original script's query param logic
-- into a method and a conceptual path_key for routing.
local function determine_route_key_from_request(request_context)
local method = request_context.method
local query = request_context.query
local path_info = request_context.path -- From PATH_INFO CGI variable
-- 1. Prioritize PATH_INFO if it is significant (not just "/" or empty)
if path_info and path_info ~= "/" and path_info ~= "" then
-- Ensure it starts with a slash, though PATH_INFO usually does.
if path_info:sub(1,1) == "/" then
return method, path_info
else
return method, "/" .. path_info
end
end
-- 2. Fallback to query parameter based routing (original logic)
-- Based on the original dispatch logic:
-- ?get_config=X -> GET /X
-- ?set_config=X -> POST /X (assuming POST for set operations)
-- ?get_status=X -> GET /X
-- ?run_action=X -> POST /X (assuming POST for actions)
-- ?method=GET&section=X (mbox-config style) -> GET /X
if query.get_config then
return "GET", "/" .. query.get_config
elseif query.set_config then
-- Most 'set' operations imply data being sent, typically via POST.
-- If a specific 'set' needs GET (unusual), a specific route could be added.
return "POST", "/" .. query.set_config
elseif query.get_status then
return "GET", "/" .. query.get_status
elseif query.run_action then
-- Actions usually imply causing a change or process, typically POST.
return "POST", "/" .. query.run_action
elseif query.method and query.method:upper() == "GET" and query.section then
return "GET", "/" .. query.section
end
-- Fallback or error if no known action parameter is found.
-- The main skyweave.lua will likely call this. If it returns nil,
-- main script can decide to return a 400 Bad Request.
-- Alternatively, router:dispatch could handle nil path_key.
return nil, nil
end
function Router:dispatch(request_context)
if not request_context then
response.error("Internal Server Error: No request context provided to dispatcher.", 500)
return
end
local determined_method, determined_path_key = determine_route_key_from_request(request_context)
if not determined_path_key then
-- This means the query parameters didn't match any known patterns
-- from the original script's dispatch logic.
response.error("Bad Request: No valid action or endpoint specified in query parameters.", 400)
return
end
local route_lookup_key = determined_method .. ":" .. determined_path_key
-- io.stderr:write("Dispatching with key: " .. route_lookup_key .. "\n") -- For debugging
local handler_func = self.routes[route_lookup_key]
if handler_func then
local success, result = pcall(handler_func, request_context)
if not success then
-- pcall failed, 'result' contains the error message/object
response.error("Handler Error: " .. tostring(result), 500)
else
-- Handler executed successfully, 'result' is the return value
if type(result) == "table" and result.async == true and result.command and result.response then
-- Asynchronous task pattern
response.json(result.response, 202) -- 202 Accepted
-- Ensure the command is not nil or empty before executing
if result.command and result.command ~= "" then
local bg_command = "(sleep 2; " .. result.command .. ") >/dev/null 2>&1 &"
os.execute(bg_command)
else
io.stderr:write("Async task error: Empty or nil command provided.\n")
-- Optionally, could update client if an error system for async tasks exists.
end
elseif type(result) == "table" and result.errCode then
-- Handler returned a structured error (common in original script)
response.json(result, result.http_status_code or 400) -- Use provided http_status_code or default to 400
elseif type(result) == "table" then
-- Standard synchronous JSON response
response.json(result, 200)
elseif result == nil then
-- Handler returned nil, could mean 204 No Content or an unhandled case.
-- For now, let's assume it's a deliberate No Content.
response.json({}, 204)
else
-- Handler returned something unexpected (e.g. a string directly)
-- This case might need refinement based on actual handler practices.
response.error("Handler returned unexpected data type: " .. type(result), 500)
end
end
else
response.error("Endpoint Not Found: No handler for " .. determined_method .. " " .. determined_path_key, 404)
end
end
return Router
+254
View File
@@ -0,0 +1,254 @@
-- skyweave/utils.lua
local json = require("json") -- Assuming json.lua is in a standard Lua path e.g. /usr/lib/lua
-- The json variable will be nil if 'json' module is not found.
-- Functions using json (like get_live_interface_info) should handle cases where json is nil.
local utils = {}
-- Moved from original main script / request.lua
function utils.parse_query_string(query_string)
local params = {}
if not query_string or query_string == "" then return params end
for pair in query_string:gmatch("[^&]+") do
local key, value = pair:match("([^=]+)=?(.*)")
if key then
key = key:gsub("+", " "):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
value = value or "" -- Ensure value is a string, even if empty after '='
value = value:gsub("+", " "):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
params[key] = value
end
end
return params
end
-- Moved from original main script
function utils.execute_command(command)
local file = io.popen(command, "r")
if not file then return nil, "popen failed for command: " .. command end
local output = file:read("*a")
local status = file:close()
-- io.popen returns a file handle or nil on error.
-- file:close() returns true, nil + error message + error number, or nil + exit status for commands.
-- We are interested if the command itself was successful (usually exit code 0)
-- For simplicity, just returning output. Robust error handling would check status.
return output
end
-- Moved from original main script
function utils.uci_run(command_str)
-- Consider adding error checking or return status if needed for os.execute
os.execute(command_str)
end
-- Moved from original main script
function utils.uci_get(path)
if not path or path == "" then return nil end
-- Sanitize path to prevent command injection if it's ever constructed from unsafe input.
-- For now, assuming path is internally generated and safe.
local f = io.popen("uci get " .. path .. " 2>/dev/null")
if f then
local value = f:read("*a")
f:close()
if value then
value = value:match("^%s*(.-)%s*$") -- Trim whitespace
if value ~= "" then
return value
end
end
end
return nil
end
-- Moved from original main script
function utils.calculate_sha256(input_string)
if not input_string then return nil end
-- Escape single quotes for safe use with echo -n 'string'
local sanitized_string = input_string:gsub("'", "'\\''")
local command = "echo -n '" .. sanitized_string .. "' | sha256sum"
local handle = io.popen(command)
if not handle then return nil end
local hash_line = handle:read("*a")
handle:close()
if hash_line then
local hash = hash_line:match("^([a-f0-9]+)") -- sha256sum output is "hash -"
return hash
end
return nil
end
-- From original main script (dependency for parse_log_line)
local months_map = {
Jan="01", Feb="02", Mar="03", Apr="04", May="05", Jun="06",
Jul="07", Aug="08", Sep="09", Oct="10", Nov="11", Dec="12"
}
-- Moved from original main script
function utils.parse_log_line(raw_line)
local entry = {
timestamp_str = "",
datetime_obj = nil, -- Will be YYYY-MM-DDTHH:MM:SS string
facility_level = "",
process_name = nil,
pid = nil,
message = "",
kernel_timestamp = nil,
raw_log = raw_line,
log_hash = utils.calculate_sha256(raw_line), -- Uses self.calculate_sha256
parse_error = nil
}
local ts_dayname, ts_mon, ts_day, ts_time, ts_year, fac_lvl, proc_tag, msg_body =
raw_line:match("^(%a+)%s+(%a+)%s+(%d+)%s+(%d%d:%d%d:%d%d)%s+(%d%d%d%d)%s+([%w%.%-]+)%s*([^:]+):%s*(.*)$")
if ts_year then
entry.timestamp_str = string.format("%s %s %s %s %s", ts_dayname, ts_mon, ts_day, ts_time, ts_year)
local month_num = months_map[ts_mon]
if month_num then
entry.datetime_obj = string.format("%s-%s-%02dT%s", ts_year, month_num, tonumber(ts_day), ts_time)
end
entry.facility_level = fac_lvl
entry.message = msg_body
local proc_name_only, pid_val = proc_tag:match("([^%[]+)%[(%d+)%]")
if proc_name_only and pid_val then
entry.process_name = proc_name_only
entry.pid = tonumber(pid_val)
else
entry.process_name = proc_tag:match("^%s*(.-)%s*$")
end
else
local kern_ts, kern_msg = raw_line:match("^%[%s*(%d+%.?%d*)%s*%]%s*(.*)$")
if kern_ts then
entry.kernel_timestamp = tonumber(kern_ts)
entry.message = kern_msg
entry.facility_level = "kernel.None"
entry.process_name = "kernel"
else
entry.message = raw_line
entry.parse_error = "Unknown log format"
end
end
return entry
end
-- Moved from original main script
function utils.get_live_interface_info(interface_name)
if not interface_name then return nil end
if not json then
-- Or return an error structure indicating JSON lib is missing
return nil, nil, nil, nil, "JSON library not available for get_live_interface_info"
end
local command = "ubus call network.interface." .. interface_name .. " status"
local output = utils.execute_command(command) -- Uses self.execute_command
if not output or output == "" then
return nil -- No output means no IP address found
end
local success, status_data = pcall(json.decode, output)
if success and status_data and status_data["ipv4-address"] and #status_data["ipv4-address"] > 0 then
local ipv4_info = status_data["ipv4-address"][1]
local device = status_data["device"]
local leasetime = status_data["data"] and status_data["data"]["leasetime"] -- Check if data and leasetime exist
return ipv4_info.address, ipv4_info.mask, device, leasetime
elseif not success then
-- Optionally, return the error message from json.decode
return nil, nil, nil, nil, "JSON decode error: " .. tostring(status_data) -- status_data is error msg on pcall failure
end
return nil
end
-- Moved from original main script
function utils.get_live_dns_servers_from_resolv(logical_interface_name)
local dns_servers = {}
local search_domains = {}
-- Ensure logical_interface_name is somewhat safe before using in string matching,
-- though it's typically from internal config.
if not logical_interface_name or not logical_interface_name:match("^[a-zA-Z0-9_%-]+$") then
return dns_servers, search_domains -- Invalid interface name format
end
local file = io.open("/tmp/resolv.conf.auto", "r")
if not file then return dns_servers, search_domains end
local in_target_section = false
local section_start_pattern_str = "# Interface " .. logical_interface_name
for line in file:lines() do
if not in_target_section then
if line:match("^%s*" .. section_start_pattern_str:gsub("%%", "%%%%") .. "%s*$") then -- Escape % for string pattern
in_target_section = true
end
else
if line:match("^%s*# Interface ") and not line:match("^%s*" .. section_start_pattern_str:gsub("%%", "%%%%") .. "%s*$") then
break
end
local ip = line:match("^%s*nameserver%s+([%d%.]+)")
if ip then table.insert(dns_servers, ip) end
local domain_list_str = line:match("^%s*search%s+(.+)")
if domain_list_str then
for domain in domain_list_str:gmatch("[^%s]+") do
table.insert(search_domains, domain)
end
end
end
end
file:close()
return dns_servers, search_domains
end
-- Moved from original main script
function utils.get_default_gateway_from_route()
local file = io.open("/proc/net/route", "r")
if not file then return nil, nil end
local gateway_ip = nil
local interface_name = nil
file:read("*l") -- Skip header
for line in file:lines() do
local iface, dest, gw_hex, flags_hex = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)")
if iface and dest and gw_hex and flags_hex then
if dest == "00000000" and gw_hex ~= "00000000" then
local flags_num = tonumber(flags_hex, 16)
-- UG flags (Up and Gateway). Typically 0x3 (binary 11) or 0x1 (binary 01 if not gateway but up)
-- We need bit 1 (value 2 for gateway) to be set.
if flags_num and math.floor(flags_num / 2) % 2 == 1 then -- Checks if the 'Gateway' bit (0x2) is set
-- Lua 5.1 doesn't have bitwise ops directly.
-- (flags_num & 2) ~= 0 is equivalent to checking if the second bit is set.
-- For example, if flags_num = 3 (0011), floor(3/2)%2 = 1%2 = 1. Correct.
-- If flags_num = 1 (0001), floor(1/2)%2 = 0%2 = 0. Correct.
-- If flags_num = 7 (0111), floor(7/2)%2 = 3%2 = 1. Correct.
-- Ensure gw_hex is 8 characters long for safety
if #gw_hex == 8 then
-- Corrected parsing for little-endian hex IP from /proc/net/route
local o4 = tonumber(string.sub(gw_hex, 1, 2), 16)
local o3 = tonumber(string.sub(gw_hex, 3, 4), 16)
local o2 = tonumber(string.sub(gw_hex, 5, 6), 16)
local o1 = tonumber(string.sub(gw_hex, 7, 8), 16)
if o1 and o2 and o3 and o4 then
gateway_ip = string.format("%d.%d.%d.%d", o4, o3, o2, o1)
interface_name = iface
break
end
end
end
end
end
end
file:close()
return gateway_ip, interface_name
end
return utils