diff --git a/src/main.lua b/src/main.lua index 083cdc7..ccdd038 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,13 +1,36 @@ +--[[ +TODO: + - network/state returns awfully little information (only station mode) + - document REST API (mention rq IDs and endpoint information, list endpoints+args+CRUD type, unknown values are empty fields) + - use a slightly more descriptive success/error definition (e.g. errortype=system/missing-arg/generic) + - how to handle requests which need a restart of uhttpd? (e.g. network/openap) + - a plain GET request (no ajax/script) runs the risk of timing out on lengthy operations: implement polling in API to get progress updates? + (this would require those operations to run in a separate daemon process which can be monitored by the CGI handler) + - protect dump function against reference loops (see: http://lua-users.org/wiki/TableSerialization, json also handles this well) + - (this is an old todo item from network:available(), might still be relevant at some point) + extend reconf interface to support function arguments (as tables) so wifihelper functionality can be integrated + but how? idea: pass x_args={arg1="a",arg2="2342"} for component 'x' + or: allow alternative for x="y" --> x={action="y", arg1="a", arg2="2342"} + in any case, arguments should be put in a new table to pass to the function (since order is undefined it must be an assoc array) + +NOTES: + - The endpoint function info in response objects is incorrect when the global function is called with a blank argument, + to cleanly solve this, module/function resolution should be moved from main() to the request object +]]-- + local l = require("logger") local RequestClass = require("rest.request") local ResponseClass = require("rest.response") +local wifi = require("network.wlanconfig") +local reconf = require("network.netconfig") -local DEBUG_PCALLS = false +--NOTE: pcall protects from invocation exceptions, which is what we need except +--during debugging. This flag replaces them with a normal call so we can inspect stack traces. +local DEBUG_PCALLS = true local postData = nil -local resp = ResponseClass.new() local function setupAutoWifiMode() @@ -26,6 +49,8 @@ local function init() local n = tonumber(os.getenv("CONTENT_LENGTH")) postData = io.read(n) end + + return wifi.init() and reconf.init(wifi, true) end --usually returns function+nil, function+number in case of number in place of function name; or @@ -96,12 +121,14 @@ end if ok == true then print(r:serializeAsJson()) else + local resp = ResponseClass.new(rq) resp:setError("call to function '" .. mod .. "/" .. sr .. "' failed") print(resp:serializeAsJson()) l:error("calling function '" .. func .. "' in API module '" .. mod .. "' somehow failed ('" .. r .. "')") end else - resp:setError("function unknown '" .. mod .. "/" .. func .. "'") + local resp = ResponseClass.new(rq) + resp:setError("function unknown '" .. (mod or "") .. "/" .. (func or "") .. "'") print(resp:serializeAsJson()) l:error("could not resolve requested API function ('" .. sr .. "')") end @@ -109,5 +136,13 @@ end end -init() -main() +if init() == false then + local resp = ResponseClass.new() + resp:setError("initialization failed") + print(resp:serializeAsJson()) --FIXME: this message does not seem to be sent + l:error("initialization failed") --NOTE: this assumes the logger has been inited properly, despite init() having failed + os.exit(1) +else + main() + os.exit(0) +end diff --git a/src/main.old.lua b/src/main.old.lua deleted file mode 100644 index 758901c..0000000 --- a/src/main.old.lua +++ /dev/null @@ -1,173 +0,0 @@ ---[[ - Response format: - ["OK" | "WARN" | "ERR"]<,{message}> - {comma-separated line 1} - ... - {comma-separated line n} - - - general info on wireless config: http://wiki.openwrt.org/doc/uci/wireless - - uci docs: http://wiki.openwrt.org/doc/techref/uci - - parse/generate urls: https://github.com/keplerproject/cgilua/blob/master/src/cgilua/urlcode.lua - - utility functions: http://luci.subsignal.org/trac/browser/luci/trunk/libs/sys/luasrc/sys.lua - - iwinfo tool source: http://luci.subsignal.org/trac/browser/luci/trunk/contrib/package/iwinfo/src/iwinfo.lua?rev=7919 - - captive portal -> redirect all web traffic to one page for auth (or network selection) - http://wiki.openwrt.org/doc/howto/wireless.hotspot - ]] ---print ("HTTP/1.0 200 OK") -io.write ("Content-type: text/plain\r\n\r\n") - -local u = require("util") -local l = require("logger") -local wifi = require("network.wlanconfig") -local reconf = require("network.netconfig") -local urlcode = require("util.urlcode") -local uci = require("uci").cursor() -local iwinfo = require("iwinfo") - -local argOperation, argDevice, argSsid, argPhrase, argRecreate -local errortext = nil - -function init() - l:init(l.LEVEL.debug, true, io.stderr) - local qs = os.getenv("QUERY_STRING") - local urlargs = {} - urlcode.parsequery(qs, urlargs) - - --supplement urlargs with arguments from the command-line - for _, v in ipairs(arg) do - local split = v:find("=") - if split ~= nil then - urlargs[v:sub(1, split - 1)] = v:sub(split + 1) - end - end - - argOperation = urlargs["op"] - argDevice = urlargs["dev"] or DFL_DEVICE - argSsid = urlargs["ssid"] - argPhrase = urlargs["phrase"] - argRecreate = urlargs["recreate"] - - if urlargs["echo"] ~= nil then - print("[[echo: '"..qs.."']]"); - end - - if argOperation == nil then - errortext = "Missing operation specifier" - return false - end - - return wifi.init() and reconf.init(wifi, true) -end - - -function main() - if argOperation == "getavl" then - local sr = wifi.getScanInfo() - local si, se - - --TODO: - -- - extend reconf interface to support function arguments (as tables) so wifihelper functionality can be integrated - -- but how? idea: pass x_args={arg1="a",arg2="2342"} for component 'x' - -- or: allow alternative for x="y" --> x={action="y", arg1="a", arg2="2342"} - -- in any case, arguments should be put in a new table to pass to the function (since order is undefined it must be an assoc array) - if sr and #sr > 0 then - u.printWithSuccess(#sr .. " network(s) found"); - for _, se in ipairs(sr) do - --print("[[ " .. u.dump(se) .. " ]]") --TEMP - if se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then - print(se.ssid .. "," .. se.bssid .. "," .. se.channel .. "," .. wifi.mapDeviceMode(se.mode) .. "," .. wifi.mapEncryptionType(se.encryption)) - end - end - else - u.exitWithError("No scan results or scanning not possible") - end - - elseif argOperation == "getknown" then - u.printWithSuccess("") - for _, net in ipairs(wifi.getConfigs()) do - if net.mode == "sta" then - local bssid = net.bssid or "" - local channel = net.channel or "" - print(net.ssid .. "," .. bssid .. "," .. channel) - end - end - - elseif argOperation == "getstate" then - local ds = wifi.getDeviceState() - local ssid = ds.ssid or "" - local bssid = ds.bssid or "" - local channel = ds.channel or "" - u.printWithSuccess(""); - print(ssid .. "," .. bssid .. "," .. channel .. "," .. ds.mode) - - elseif argOperation == "assoc" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to associate with") end - - local cfg = nil - for _, net in ipairs(wifi.getConfigs()) do - if net.mode ~= "ap" and net.ssid == argSsid then - cfg = net - break - end - end - if cfg == nil or argRecreate ~= nil then - local scanResult = wifi.getScanInfo(argSsid) - if scanResult ~= nil then - wifi.createConfigFromScanInfo(scanResult, argPhrase) - else - --check for error - u.exitWithError("No wireless network with SSID '" .. argSsid .. "' is available") - end - end - wifi.activateConfig(argSsid) - reconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } - u.exitWithSuccess("Wlan associated with network " .. argSsid .. "!") - - elseif argOperation == "disassoc" then - wifi.activateConfig() - local rv = wifi.restart() - u.exitWithSuccess("Deactivated all wireless networks [$?=" .. rv .. "]") - - elseif argOperation == "openap" then - --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed - reconf.switchConfiguration{apnet="add_noreload"} - wifi.activateConfig(wifi.AP_SSID) - reconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } - u.exitWithSuccess("Switched to AP mode (SSID: '" .. wifi.AP_SSID .. "')") - - elseif argOperation == "rm" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to remove") end - if wifi.removeConfig(argSsid) then - u.exitWithSuccess("Removed wireless network with SSID " .. argSsid) - else - u.exitWithWarning("No wireless network with SSID " .. argSsid) - end - - elseif argOperation == "test" then - --invert actions performed by openap operation - reconf.switchConfiguration{ apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm" } --- reconf.switchConfiguration{dnsredir="add"} - u.exitWithSuccess("nop") - - elseif argOperation == "auto" then - u.exitWithWarning("Not implemented"); - --scan nets - --take union of scan and known - --connect to first if not empty; setup ap otherwise - - else - u.exitWithError("Unknown operation: '" .. argOperation .. "'") - end - - os.exit(0) -end - - - ---[[ START OF CODE ]]-- - -if init() == false then - u.exitWithError(errortext) -end - -main() diff --git a/src/rest/api/api_network.lua b/src/rest/api/api_network.lua index a1a0927..47097f5 100644 --- a/src/rest/api/api_network.lua +++ b/src/rest/api/api_network.lua @@ -1,95 +1,179 @@ local l = require("logger") +local u = require("util.utils") +local netconf = require("network.netconfig") +local wifi = require("network.wlanconfig") +local ResponseClass = require("rest.response") local M = {} M.isApi = true function M._global(d) - return "not implemented..." + local r = ResponseClass.new(d) + r:setError("not implemented") + return r end ---[[ - if argOperation == "getavl" then - local sr = wifi.getScanInfo() - local si, se - - --TODO: - -- - extend reconf interface to support function arguments (as tables) so wifihelper functionality can be integrated - -- but how? idea: pass x_args={arg1="a",arg2="2342"} for component 'x' - -- or: allow alternative for x="y" --> x={action="y", arg1="a", arg2="2342"} - -- in any case, arguments should be put in a new table to pass to the function (since order is undefined it must be an assoc array) - if sr and #sr > 0 then - u.printWithSuccess(#sr .. " network(s) found"); - for _, se in ipairs(sr) do - --print("[[ " .. u.dump(se) .. " ]]") --TEMP - if se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then - print(se.ssid .. "," .. se.bssid .. "," .. se.channel .. "," .. wifi.mapDeviceMode(se.mode) .. "," .. wifi.mapEncryptionType(se.encryption)) - end +--accepts API argument 'nofilter'(bool) to disable filtering of APs and 'self' +function M.available(d) + local r = ResponseClass.new(d) + local noFilter = u.toboolean(d:get("nofilter")) + local sr = wifi.getScanInfo() + local si, se + + if sr and #sr > 0 then + r:setSuccess("") + local netInfoList = {} + for _, se in ipairs(sr) do + if noFilter or se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then + local netInfo = {} + + netInfo["ssid"] = se.ssid + netInfo["bssid"] = se.bssid + netInfo["channel"] = se.channel + netInfo["mode"] = wifi.mapDeviceMode(se.mode) + netInfo["encryption"] = wifi.mapEncryptionType(se.encryption) + netInfo["signal"] = se.signal + netInfo["quality"] = se.quality + netInfo["quality_max"] = se.quality_max + --netInfo["raw"] = l:dump(se) --TEMP for debugging only + + table.insert(netInfoList, netInfo) end + end + r:addData("count", #netInfoList) + r:addData("networks", netInfoList) + else + r:setError("No scan results or scanning not possible") + end + + return r +end + +--accepts API argument 'nofilter'(bool) to disable filtering of APs and 'self' +function M.known(d) + local r = ResponseClass.new(d) + local noFilter = u.toboolean(d:get("nofilter")) + + r:setSuccess() + local netInfoList = {} + for _, net in ipairs(wifi.getConfigs()) do + if noFilter or net.mode == "sta" then + local netInfo = {} + netInfo["ssid"] = net.ssid + netInfo["bssid"] = net.bssid or "" + netInfo["channel"] = net.channel or "" + netInfo["encryption"] = net.encryption + netInfo["raw"] = l:dump(net) --TEMP for debugging only + table.insert(netInfoList, netInfo) + end + end + r:addData("count", #netInfoList) + r:addData("networks", netInfoList) + + return r +end + +function M.state(d) + local r = ResponseClass.new(d) + local ds = wifi.getDeviceState() + + r:setSuccess() + r:addData("ssid", ds.ssid or "") + r:addData("bssid", ds.bssid or "") + r:addData("channel", ds.channel or "") + r:addData("mode", ds.mode) + r:addData("raw", l:dump(ds)) --TEMP for debugging only + + return r +end + +--UNTESTED +--requires ssid(string), accepts phrase(string), recreate(bool) +function M.assoc(d) + local r = ResponseClass.new(d) + local argSsid = d:get("ssid") + local argPhrase = d:get("phrase") + local argRecreate = d:get("recreate") + + if argSsid == nil or argSsid == "" then + r:setError("missing ssid argument") + return r + end + + local cfg = nil + for _, net in ipairs(wifi.getConfigs()) do + if net.mode ~= "ap" and net.ssid == argSsid then + cfg = net + break + end + end + + if cfg == nil or argRecreate ~= nil then + local scanResult = wifi.getScanInfo(argSsid) + if scanResult ~= nil then + wifi.createConfigFromScanInfo(scanResult, argPhrase) else - u.exitWithError("No scan results or scanning not possible") + --check for error + r:setError("no wireless network with requested SSID is available") + r:addData("ssid", argSsid) end + end - elseif argOperation == "getknown" then - u.printWithSuccess("") - for _, net in ipairs(wifi.getConfigs()) do - if net.mode == "sta" then - local bssid = net.bssid or "" - local channel = net.channel or "" - print(net.ssid .. "," .. bssid .. "," .. channel) - end - end + wifi.activateConfig(argSsid) + netconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } + r:setSuccess("wlan associated") + r:addData("ssid", argSsid) - elseif argOperation == "getstate" then - local ds = wifi.getDeviceState() - local ssid = ds.ssid or "" - local bssid = ds.bssid or "" - local channel = ds.channel or "" - u.printWithSuccess(""); - print(ssid .. "," .. bssid .. "," .. channel .. "," .. ds.mode) + return r +end + +--UNTESTED +function M.disassoc(d) + local r = ResponseClass.new(d) - elseif argOperation == "assoc" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to associate with") end - - local cfg = nil - for _, net in ipairs(wifi.getConfigs()) do - if net.mode ~= "ap" and net.ssid == argSsid then - cfg = net - break - end - end - if cfg == nil or argRecreate ~= nil then - local scanResult = wifi.getScanInfo(argSsid) - if scanResult ~= nil then - wifi.createConfigFromScanInfo(scanResult, argPhrase) - else - --check for error - u.exitWithError("No wireless network with SSID '" .. argSsid .. "' is available") - end - end - wifi.activateConfig(argSsid) - reconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } - u.exitWithSuccess("Wlan associated with network " .. argSsid .. "!") + wifi.activateConfig() + local rv = wifi.restart() + r:setSuccess("all wireless networks deactivated") + r:addData("wifi_restart_result", rv) - elseif argOperation == "disassoc" then - wifi.activateConfig() - local rv = wifi.restart() - u.exitWithSuccess("Deactivated all wireless networks [$?=" .. rv .. "]") + return r +end + +--UNTESTED +function M.openap(d) + local r = ResponseClass.new(d) + + --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed + netconf.switchConfiguration{apnet="add_noreload"} + wifi.activateConfig(wifi.AP_SSID) + netconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } + r:setSuccess("switched to Access Point mode") + r:addData("ssid", wifi.AP_SSID) - elseif argOperation == "openap" then - --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed - reconf.switchConfiguration{apnet="add_noreload"} - wifi.activateConfig(wifi.AP_SSID) - reconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } - u.exitWithSuccess("Switched to AP mode (SSID: '" .. wifi.AP_SSID .. "')") + return r +end + +--UNTESTED +--requires ssid(string) +function M.rm(d) + local r = ResponseClass.new(d) + local argSsid = d:get("ssid") - elseif argOperation == "rm" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to remove") end - if wifi.removeConfig(argSsid) then - u.exitWithSuccess("Removed wireless network with SSID " .. argSsid) - else - u.exitWithWarning("No wireless network with SSID " .. argSsid) - end -]-- + if argSsid == nil or argSsid == "" then + r:setError("missing ssid argument") + return r + end + + if wifi.removeConfig(argSsid) then + r:setSuccess("removed wireless network with requested SSID") + r:addData("ssid", argSsid) + else + r:setError("no wireless network with requested SSID") --this used to be a warning instead of an error... + r:addData("ssid", argSsid) + end + + return r +end return M diff --git a/src/rest/api/api_test.lua b/src/rest/api/api_test.lua index 2f21e60..02bf79f 100644 --- a/src/rest/api/api_test.lua +++ b/src/rest/api/api_test.lua @@ -6,26 +6,29 @@ local M = {} M.isApi = true function M._global(d) - local r = ResponseClass.new() - local ba = d:getBlankArgument() or "" - r:setSuccess("REST test API - default function called with blank argument: '" .. ba .. "'") + local r = ResponseClass.new(d) + local ba = d:getBlankArgument() + + r:setSuccess("REST test API - default function called with blank argument: '" .. (ba or "") .. "'") + if ba ~= nil then r:addData("blank_argument", ba) end + return r end function M.success(d) - local r = ResponseClass.new() - r:setSuccess("yay!") + local r = ResponseClass.new(d) + r:setSuccess() return r end function M.error(d) - local r = ResponseClass.new() + local r = ResponseClass.new(d) r:setError("this error has been generated on purpose") return r end function M.echo(d) - local r = ResponseClass.new() + local r = ResponseClass.new(d) r:setSuccess("request echo") r:addData("request_data", d) return r diff --git a/src/rest/request.lua b/src/rest/request.lua index faa025e..e5f61a3 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -59,6 +59,9 @@ function M.new(postData, debug) self.apiFunction = self.cmdLineArgs["f"] or self.apiFunction end + if self.apiModule == "" then self.apiModule = nil end + if self.apiFunction == "" then self.apiFunction = nil end + return self end diff --git a/src/rest/response.lua b/src/rest/response.lua index ed6001e..3f34511 100644 --- a/src/rest/response.lua +++ b/src/rest/response.lua @@ -3,17 +3,31 @@ local JSON = (loadfile "util/JSON.lua")() local M = {} M.__index = M +local REQUEST_ID_ARGUMENT = "rq_id" +local INCLUDE_ENDPOINT_INFO = false + setmetatable(M, { __call = function(cls, ...) return cls.new(...) end }) -function M.new() +--requestObject should always be passed (except on init failure, when it is not yet available) +function M.new(requestObject) local self = setmetatable({}, M) self.body = {status = nil, data = {}} + if requestObject ~= nil then + local rqId = requestObject:get(REQUEST_ID_ARGUMENT) + if rqId ~= nil then self.body[REQUEST_ID_ARGUMENT] = rqId end + + if INCLUDE_ENDPOINT_INFO == true then + self.body["module"] = requestObject:getApiModule() + self.body["function"] = requestObject:getApiFunction() or "" + end + end + return self end @@ -23,12 +37,12 @@ end function M:setSuccess(msg) self.body.status = "success" - self.body.msg = msg + if msg ~= "" then self.body.msg = msg end end function M:setError(msg) self.body.status = "error" - self.body.msg = msg + if msg ~= "" then self.body.msg = msg end end --NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key diff --git a/src/util/utils.lua b/src/util/utils.lua index fe6adb3..f315384 100644 --- a/src/util/utils.lua +++ b/src/util/utils.lua @@ -2,6 +2,13 @@ local uci = require("uci").cursor() local M = {} +function M.toboolean(s) + if not s then return false end + + local b = s:lower() + return (b == "1" or b == "t" or b == "true") and true or false +end + function M.getUciSectionName(config, type) local sname = nil uci:foreach(config, type, function(s) sname = s[".name"] end)