Initial commit of code. All endpoints tested except for set_connect_wifi()
This commit is contained in:
@@ -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
@@ -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
|
||||
Executable
+611
@@ -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
|
||||
Reference in New Issue
Block a user