Initial commit of mico-framework refactor. Original monolithic script still exists in /src
This commit is contained in:
Executable
+156
@@ -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
|
||||
Executable
+63
@@ -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
|
||||
Executable
+149
@@ -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
|
||||
Executable
+103
@@ -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
|
||||
Executable
+44
@@ -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
|
||||
Executable
+134
@@ -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§ion=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
|
||||
Executable
+254
@@ -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
|
||||
Reference in New Issue
Block a user