Initial commit of code. All endpoints tested except for set_connect_wifi()

This commit is contained in:
wes
2025-05-28 08:06:31 -04:00
parent 8dfefac310
commit 189afaeb45
3 changed files with 1060 additions and 0 deletions
Executable
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# skyweave_deploy.sh - Deploy Lua API script to RVLink roof unit (legacy crypto)
ROOF_IP="192.168.10.254"
ROOF_USER="root"
REMOTE_PATH="/www/cgi-bin/skyweave.lua"
SSH_PORT="22"
REMOTE_HTTP_PORT="${2:-42069}" # Default to 42069 if not specified
# Local script path (default to skyweave.lua in current directory)
LOCAL_SCRIPT="${1:-src/skyweave.lua}"
# SSH/SCP legacy crypto options
SSH_OPTS="-oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa -oMACs=+hmac-sha1 -p ${SSH_PORT}"
SCP_OPTS="-O -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa -oMACs=+hmac-sha1 -P ${SSH_PORT}"
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Deploying ${LOCAL_SCRIPT} to ${ROOF_USER}@${ROOF_IP}:${REMOTE_PATH}...${NC}"
# Check if local script exists
if [ ! -f "${LOCAL_SCRIPT}" ]; then
echo -e "${RED}Error: Local script ${LOCAL_SCRIPT} not found!${NC}"
exit 1
fi
# Copy the script to the roof unit using legacy SCP
scp ${SCP_OPTS} "${LOCAL_SCRIPT}" ${ROOF_USER}@${ROOF_IP}:${REMOTE_PATH}
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to copy script to roof unit!${NC}"
exit 1
fi
# Make the script executable using legacy SSH
ssh ${SSH_OPTS} ${ROOF_USER}@${ROOF_IP} "chmod +x ${REMOTE_PATH}"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to make script executable!${NC}"
exit 1
fi
echo -e "${GREEN}Deployment successful!${NC}"
# Test the API
echo -e "${YELLOW}Testing API by fetching network configuration...${NC}"
RESPONSE=$(curl -s -X GET "http://${ROOF_IP}:${REMOTE_HTTP_PORT}/cgi-bin/skyweave.lua?get_config=network_config")
if [ $? -eq 0 ] && [[ "${RESPONSE}" == *'"errCode":0'* ]]; then
echo -e "${GREEN}API test successful!${NC}"
echo -e "Response: ${RESPONSE}"
else
echo -e "${RED}API test failed!${NC}"
echo -e "Curl exit status: $?"
echo -e "Response: ${RESPONSE}"
fi
echo -e "\n${YELLOW}API endpoint: http://${ROOF_IP}:${REMOTE_HTTP_PORT}/cgi-bin/skyweave.lua${NC}"
echo -e "${YELLOW}Example usage: curl -X POST -H \"Content-Type: application/json\" -d '{\"command\":\"your_command\"}' http://${ROOF_IP}:${REMOTE_HTTP_PORT}/cgi-bin/skyweave.lua${NC}"
Executable
+388
View File
@@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json
+611
View File
@@ -0,0 +1,611 @@
#!/usr/bin/lua
-- Load required libraries
local json = require("json") -- This is in /usr/lib/lua/json.lua on my system
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"
}
local function send_json_response(data)
print("Content-Type: application/json")
print("") -- End of headers
print(json.encode(data))
end
local function send_error_response(errMsg, errCode, http_status)
-- http_status for CGI isn't standard like in HTTP servers but good for structure
print("Content-Type: application/json")
if http_status then
print("Status: " .. http_status) -- e.g., "400 Bad Request", "404 Not Found"
end
print("") -- End of headers
print(json.encode({ errCode = errCode or -1, errMsg = errMsg or "Unknown error" }))
end
-- Function to execute system commands and return the output
local function execute_command(command)
local file = io.popen(command, "r")
local output = file:read("*a")
file:close()
return output
end
local function uci_run(command_str)
os.execute(command_str)
end
-- Function to safely get UCI values
local function uci_get(path)
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
-- Function to parse query string
local function parse_query_string(query_string)
local params = {}
if not query_string then return params end
for pair in query_string:gmatch("[^&]+") do
local key, value = pair:match("([^=]+)=?(.*)")
if key then
-- URL decode the key and value
key = key:gsub("+", " "):gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end)
value = value:gsub("+", " "):gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end)
params[key] = value
end
end
return params
end
function 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
function 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 = calculate_sha256(raw_line),
parse_error = nil
}
-- Try parsing standard syslog format first
-- Example: Tue May 27 21:57:25 2025 daemon.err uhttpd[4029]: message
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 -- Standard syslog format matched
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+)%]") -- process[pid]
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*$") -- Just process name, no PID found in brackets
end
else -- Try parsing kernel log format
-- Example: [ 123.456789] Kernel message body
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" -- Assign a facility for kernel messages
entry.process_name = "kernel"
else
-- If neither format matches, store the raw line as message and set parse_error
entry.message = raw_line
entry.parse_error = "Unknown log format"
end
end
return entry
end
-----------------------------------------------------------------------
-- Command parsers
-----------------------------------------------------------------------
function get_live_interface_info(interface_name)
-- interface_name would be the logical OpenWrt interface, e.g., "wan"
-- This often maps to a device like "vif-sta0" or "eth0.2" etc.
local command = "ubus call network.interface." .. interface_name .. " status"
local output = execute_command(command)
if not output or output == "" then
return nil -- No output means no IP address found
end
-- Attempt to decode the JSON output
local status_data, err = json.decode(output)
if status_data and status_data["ipv4-address"] and #status_data["ipv4-address"] > 0 then
return status_data["ipv4-address"][1].address, status_data["ipv4-address"][1].mask, status_data["device"], status_data["data"].leasetime -- Returns the first IPv4 address
end
return nil
end
-----------------------------------------------------------------------
-- Configuration Parsers
-----------------------------------------------------------------------
local function get_live_dns_servers_from_resolv(logical_interface_name)
local dns_servers = {}
local search_domains = {}
local file = io.open("/tmp/resolv.conf.auto", "r")
if not file then
return dns_servers, search_domains
end
local in_target_section = false
-- Construct the exact string to look for to identify the start of the section
local section_start_pattern_str = "# Interface " .. logical_interface_name
for line in file:lines() do
if not in_target_section then
-- Match the line exactly, allowing for leading/trailing whitespace on the comment itself
if line:match("^%s*" .. section_start_pattern_str .. "%s*$") then
in_target_section = true
end
else
-- If we are in the target section and encounter another interface's section, stop.
-- Check for any line starting with "# Interface " that isn't our target section
if line:match("^%s*# Interface ") and not line:match("^%s*" .. section_start_pattern_str .. "%s*$") then
break -- Exited target section
end
-- Look for nameserver entries
local ip = line:match("^%s*nameserver%s+([%d%.]+)")
if ip then
table.insert(dns_servers, ip)
end
-- Look for search domain entries
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
function 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)
-- Pure Lua check for (flags_num & 2) ~= 0
if flags_num and (math.fmod(flags_num, 4) >= 2) then -- MODIFIED LINE
local o1 = tonumber(string.sub(gw_hex, 1, 2), 16)
local o2 = tonumber(string.sub(gw_hex, 3, 4), 16)
local o3 = tonumber(string.sub(gw_hex, 5, 6), 16)
local o4 = 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", o1, o2, o3, o4)
interface_name = iface
break
end
end
end
end
end
file:close()
return gateway_ip, interface_name
end
-----------------------------------------------------------------------
-- Endpoint Handlers
-----------------------------------------------------------------------
local handlers = {}
function handlers.get_network_config(params)
local iface = params.interface or "wan" -- Default to "wan" if not specified
local live_dns_list, live_search_domains = get_live_dns_servers_from_resolv(iface)
local live_gateway_ip, live_gateway_iface = get_default_gateway_from_route()
local live_wan_ip, live_wan_netmask, ifname, dhcp_lease_time = get_live_interface_info(iface)
local response_data = { errCode = 0, errMsg = "OK"}
response_data.wwan = {
ssid = uci_get("wireless.wwan.ssid") or "",
bssid = uci_get("wireless.wwan.bssid") or "",
encryption = uci_get("wireless.wwan.encryption") or "",
key = uci_get("wireless.wwan.key") or "",
device = uci_get("wireless.wwan.device") or "",
wan_dns1 = live_dns_list[1] or "",
wan_dns2 = 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 "",
wan_iface = iface,
wan_dhcp_lease_time = dhcp_lease_time or ""
}
response_data.lan = {
ipaddr = uci_get("network.lan.ipaddr") or "",
netmask = uci_get("network.lan.netmask") or "",
gateway = uci_get("network.lan.gateway") or "",
start = uci_get("dhcp.lan.start") or "",
limit = uci_get("dhcp.lan.limit") or "",
leasetime = uci_get("dhcp.lan.leasetime") or "",
domain = uci_get("dhcp.@dnsmasq[0].domain") or "",
ignore = uci_get("dhcp.lan.ignore") or ""
}
return response_data
end
function handlers.get_wifi_scan(params)
local ifname = params.ifname or "radio0" -- Default to radio0 if not specified
-- 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, 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)", result_count = 0, results = {} }
end
local results_list = {}
local current_ap_data = nil
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
-- Ensure all required fields for the previous AP are defaulted if missing
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 -- Default signal if not found
current_ap_data.mode = current_ap_data.mode or "" -- Default mode 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
-- Updated to capture Mode, Channel, Signal, Quality
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 -- Capture Mode
current_ap_data.mode = mode_val
if channel_val then -- Capture Channel (often on the same line as Mode)
current_ap_data.channel = tonumber(channel_val)
end
elseif signal_val_dbm then -- Capture Signal
current_ap_data.signal = tonumber(signal_val_dbm)
if quality_num_str and quality_max_str then -- Also process quality if on this line
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 -- Remains nil if not found, or you can default to 0/""
table.insert(results_list, current_ap_data)
end
-- Ensure all required fields for the final JSON, including new ones
for i, ap_data in ipairs(results_list) do
results_list[i].ssid = ap_data.ssid or ""
results_list[i].address = ap_data.address -- Should always be there
results_list[i].quality = ap_data.quality or 0
results_list[i].encryption = ap_data.encryption or "none"
results_list[i].signal = ap_data.signal or 0 -- dBm value
results_list[i].mode = ap_data.mode or ""
results_list[i].channel = ap_data.channel -- number, or nil/"" if not parsed
end
return {
result_count = #results_list,
results = results_list,
errCode = 0,
errMsg = "OK",
configDone = false
}
end
function handlers.get_system_log_parsed(params) -- Renamed to distinguish
local command = "logread"
local handle = io.popen(command, "r")
if not handle then
return {
errCode = -20,
errMsg = "Failed to execute logread command.",
parsed_logs = {},
processing_summary = "Error executing logread."
}
end
local log_content = handle:read("*a")
handle:close()
local parsed_logs_list = {}
local lines_processed = 0
local lines_failed_parse = 0
if log_content and log_content ~= "" then
for line in string.gmatch(log_content, "[^\r\n]+") do
if line ~= "" then -- Process non-empty lines
lines_processed = lines_processed + 1
-- Use M.parse_log_line or conf_parsers.parse_log_line
local parsed_entry = 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 = 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,
errCode = 0,
errMsg = "OK",
configDone = false
}
return response_data
end
function handlers.set_connect_wifi(params)
local ssid = params.ssid
local bssid = params.bssid
local encryption_type = params.encryption -- maps to UCI 'encryption' option
local key = params.key
local sta_device = params.device or "radio0" -- The physical radio for the STA interface (e.g., radio0)
-- The UCI section for the Wi-Fi STA interface is typically 'wwan' in your config.
-- If it's a dynamically generated section name like 'cfgXYZ', this would need adjustment,
-- but 'wireless.wwan' is a common convention for the primary client interface.
local sta_uci_section = "wireless.wwan"
-- The ifname for this STA section, often 'vif-sta0' or derived.
-- Your uci_show.txt uses 'vif-sta0' for wireless.wwan.ifname.
local sta_ifname = "vif-sta0"
if not ssid or ssid == "" then
return { errCode = -30, errMsg = "SSID is required for Wi-Fi connection.", configDone = false }
end
-- 1. Configure the Wireless STA Interface (e.g., wireless.wwan)
uci_run("uci set " .. sta_uci_section .. ".disabled=0") -- Ensure the interface is enabled
uci_run("uci set " .. sta_uci_section .. ".mode=sta")
uci_run("uci set " .. sta_uci_section .. ".ifname=" .. sta_ifname) -- Set kernel interface name
uci_run("uci set " .. sta_uci_section .. ".device='" .. sta_device .. "'")
uci_run("uci set " .. sta_uci_section .. ".network=wan") -- Associate with the 'wan' firewall zone/network
uci_run("uci set " .. sta_uci_section .. ".ssid='" .. ssid .. "'")
if bssid and bssid ~= "" then
uci_run("uci set " .. sta_uci_section .. ".bssid='" .. bssid .. "'")
else
uci_run("uci delete " .. sta_uci_section .. ".bssid") -- Remove BSSID if not specified or empty
end
-- Handle Encryption Settings
if encryption_type == "none" or encryption_type == "" or encryption_type == nil then
uci_run("uci set " .. sta_uci_section .. ".encryption=none")
uci_run("uci delete " .. sta_uci_section .. ".key")
elseif encryption_type == "psk2" then -- WPA2 PSK
uci_run("uci set " .. sta_uci_section .. ".encryption=psk2")
uci_run("uci set " .. sta_uci_section .. ".key='" .. (key or "") .. "'")
elseif encryption_type == "psk" then -- WPA PSK
uci_run("uci set " .. sta_uci_section .. ".encryption=psk")
uci_run("uci set " .. sta_uci_section .. ".key='" .. (key or "") .. "'")
elseif encryption_type == "wpa2" then -- WPA2 Enterprise (EAP) - often needs more settings
uci_run("uci set " .. sta_uci_section .. ".encryption=wpa2")
-- Enterprise usually requires .eap_type, .identity, .password etc.
-- This simplified version assumes key might be password for some EAP methods,
-- or more likely, this case needs more specific UCI options.
if key and key ~= "" then uci_run("uci set " .. sta_uci_section .. ".password='" .. key .. "'") end
elseif encryption_type == "wpa" then -- WPA Enterprise
uci_run("uci set " .. sta_uci_section .. ".encryption=wpa")
if key and key ~= "" then uci_run("uci set " .. sta_uci_section .. ".password='" .. key .. "'") end
elseif encryption_type == "wep-open" or encryption_type == "wep-shared" or encryption_type == "wep" then
uci_run("uci set " .. sta_uci_section .. ".encryption=" .. (encryption_type == "wep" and "wep-open" or encryption_type)) -- Default WEP to wep-open
uci_run("uci set " .. sta_uci_section .. ".key='" .. (key or "") .. "'")
-- For WEP, you might need to set 'key<index>' if specific key index is used.
-- uci_run("uci set " .. sta_uci_section .. ".key1='" .. (key or "") .. "'") -- Example if key1 is used
-- uci_run("uci set " .. sta_uci_section .. ".key_index=1")
else
-- Unknown encryption type, default to 'none' or return an error
-- For safety, defaulting to 'none' and no key.
uci_run("uci set " .. sta_uci_section .. ".encryption=none")
uci_run("uci delete " .. sta_uci_section .. ".key")
-- Optionally, return an error for unsupported types:
-- return { errCode = -31, errMsg = "Unsupported encryption type: " .. tostring(encryption_type), configDone = false }
end
-- 2. Configure the Network WAN Interface
uci_run("uci set network.wan.ifname='" .. sta_ifname .. "'") -- Ensure WAN uses the Wi-Fi STA interface
uci_run("uci set network.wan.proto=dhcp") -- Set to DHCP client mode
-- Clean up any potentially conflicting static IP settings on the WAN interface
uci_run("uci delete network.wan.ipaddr")
uci_run("uci delete network.wan.netmask")
uci_run("uci delete network.wan.gateway")
uci_run("uci delete network.wan.dns")
uci_run("uci delete network.wan.ip6addr") -- Also clear potential static IPv6
uci_run("uci delete network.wan.ip6gw")
uci_run("uci delete network.wan.ip6prefix")
-- 3. Commit UCI Changes
uci_run("uci commit wireless")
uci_run("uci commit network")
-- 4. Apply Changes by Reloading Services
-- This sequence generally works well.
uci_run("wifi reload") -- Reloads wireless configuration and drivers
os.execute("sleep 3") -- Brief pause to allow wifi to settle (optional but can help)
uci_run("/etc/init.d/network reload") -- Reloads network configuration, brings WAN up
-- Return success
return {
errCode = 0,
errMsg = "OK (Wi-Fi connection process initiated for SSID: " .. ssid .. ")",
configDone = true -- Indicates a configuration change was made and applied
}
end
-----------------------------------------------------------------------
-- Main Dispatch Logic
-----------------------------------------------------------------------
-- Get request method and query string
local request_method = os.getenv("REQUEST_METHOD")
local query_string = os.getenv("QUERY_STRING")
local request_params = {}
if query_string then
request_params = parse_query_string(query_string)
end
-- Determine the main action and the specific section/command
local main_action_key = nil
local requested_endpoint_name = nil
-- Example: Prioritize 'get_config', then 'set_config', etc.
if request_params.get_config then
main_action_key = "get_" .. request_params.get_config
requested_endpoint_name = request_params.get_config
elseif request_params.set_config then
main_action_key = "set_" .. request_params.set_config
requested_endpoint_name = request_params.set_config
-- Add more top-level actions like 'get_status', 'run_action'
elseif request_params.method == "GET" and request_params.section then -- For compatibility with mbox-config style
main_action_key = "get_" .. request_params.section -- You might prefix it like handlers.get_guide_config
requested_endpoint_name = request_params.section
-- Potentially map "get_guide_config" to a function named "handlers.get_guide_config"
-- Or simply use request_params.section as the key if your handlers are named that way.
else
send_error_response("No valid action specified.", -1, "400 Bad Request")
return -- Stop script execution
end
-- Dispatch to the appropriate handler
-- You might want to make the handler key more specific if needed, e.g., "get_guide_config"
-- For this example, let's assume handlers are named like "get_guide_config", "get_lte_status"
-- And your query might be ?get_config=guide_config or ?get_status=lte
-- So, main_action_key would be, for example, `get_guide_config`
-- A more direct mapping if your handler table keys match the value of get_config/get_status:
-- Example: ?get_config=guide_config --> handler_key = "guide_config"
-- function_to_call = handlers[handler_key]
local actual_handler_key_parts = {}
if request_params.get_config then
actual_handler_key_parts[#actual_handler_key_parts + 1] = "get"
actual_handler_key_parts[#actual_handler_key_parts + 1] = request_params.get_config
elseif request_params.get_status then
actual_handler_key_parts[#actual_handler_key_parts + 1] = "get"
actual_handler_key_parts[#actual_handler_key_parts + 1] = request_params.get_status
end
-- Add other primary actions (set_config, run_action)
-- Construct the handler name, e.g. "get_guide_config"
local handler_name_to_call = table.concat(actual_handler_key_parts, "_")
if handlers[handler_name_to_call] then
local result_data = handlers[handler_name_to_call](request_params)
send_json_response(result_data)
else
send_error_response("Endpoint not found: " .. (handler_name_to_call or "N/A"), -2, "404 Not Found")
end