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
+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