diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..e1909d0 --- /dev/null +++ b/deploy.sh @@ -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}" diff --git a/lib/json.lua b/lib/json.lua new file mode 100755 index 0000000..711ef78 --- /dev/null +++ b/lib/json.lua @@ -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 diff --git a/src/skyweave.lua b/src/skyweave.lua new file mode 100755 index 0000000..1516d61 --- /dev/null +++ b/src/skyweave.lua @@ -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' 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 \ No newline at end of file