diff --git a/TODO.md b/TODO.md index 2a55afe..868daa0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,12 @@ -# TODO (from api_info.lua) - - 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 - FIX: handle mod/func resolution and calling in request:handle() which returns a response object - then send it using response:send("json") - - fix up init script handling as described here: http://wiki.openwrt.org/doc/devel/packages#packaging.a.service - - versioning/updating +# TODO (new functionality) + - save requested mod+func in request, as well as resolved function/pretty print version/function pointer (or subset…); then fix endpoint function name (when called with blank argument) in response objects to show pretty print name + - fix init script handling as described here: http://wiki.openwrt.org/doc/devel/packages#packaging.a.service + - write a simple client script to autotest as much of the api as possible + extend the client script to run arbitrary post/get requests + - document REST API (mention rq IDs and endpoint information, list endpoints+args+CRUD type, unknown values are empty fields) + (describe fail/error difference: fail is valid rq..could not comply, while error is invalid rq _or_ system error) + - use a slightly more descriptive success/error definition (e.g. errortype=system/missing-arg/generic) + - steps to take regarding versioning/updating * versioning scheme * create feed location (e.g. www.doodle3d.com/firmware/packages) (see here: http://wiki.openwrt.org/doc/packages#third.party.packages) * create opkg (already present in bin/ar71xx/packages as .ipk file) @@ -13,42 +15,38 @@ * determine how opkg decides what is 'upgradeable' * at this point manual updating should be possible, now find out how to implement in lua (execve? or write a minimalistic binding to libopkg?) * expose through info API and/or system API; also provide a way (future) to flash a new image - - dynamic AP name based on partial MAC + - dynamic AP name based on partial MAC (set once on installation and then only upon explicit request? (e.g. api/config/wifiname/default)) - require api functions which change state to be invoked as post request - - add functions to test network connectivity in steps (ifup? hasip? resolve? ping?) to network or test + - add API functions to test network connectivity in steps (ifup? hasip? resolve? ping?) to network or test + - add more config options to package, which should act as defaults for a config file on the system; candidates: + reconf.WWW_RENAME_NAME, wifihelper.{AP_ADDRESS, AP_NETMASK, (NET)} + + + +# Ideas / issues to work out + - generally, for configuration keys, it could be a good idea to use the concept of default values so it's always possible to return to a 'sane default config' - add system api module? with check-updates/do-update/etc - - write a simple client script to autotest as much of the api as possible - - extend the client script to run arbitrary post/get requests - - -# TODO (from main.lua) - - document REST API (mention rq IDs and endpoint information, list endpoints+args+CRUD type, unknown values are empty fields) - (describe fail/error difference: fail is valid rq..could not comply, while error is invalid rq _or_ system error) - - 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) (!!!is this true? it could very well be caused by a uhttpd restart) - - protect dump function against reference loops (see: http://lua-users.org/wiki/TableSerialization, json also handles this well) + - licensing (also for hardware and firmware) + credits for external code and used ideas + - (this is an old todo item from network:available(), might still be relevant at some point) extend netconf 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) + - perhaps opkg+safeboot could be useful in the update mechanism? + - add config option to compile sources using luac _or_ add an option to minify the lua code -# TODO -- in captive portal mode, https is not redirected -- see if updating works when a new version is 'released' (probably needs a real feed) -- perhaps opkg+safeboot could be useful in the update mechanism? -- licensing (also for hardware and firmware) + credits for external code and used ideas - http://www.codinghorror.com/blog/2007/04/pick-a-license-any-license.html -- add more config options to package, which should act as defaults for a config file on the system; candidates: - reconf.WWW_RENAME_NAME, wifihelper.{AP_ADDRESS, AP_NETMASK, (NET)} - https://github.com/2ion/ini.lua -- add config option to compile sources using luac _or_ add an option to minify the lua code -- add dependency on either wr703n or mr3020 (and probably comment it out again to avoid being to conservative...) -- relocatabilty of package (take root prefix into consideration everywhere) +# Bugs + - in captive portal mode, https is not redirected + - using iwinfo with interface name 'radio0' yields very little 'info' output while wlan0 works fine. + However, sometimes wlan0 disappears (happened after trying to associate with non-existing network)...why? + - protect dump function against reference loops (see , json also handles this well) + - relocatabilty of package (take root prefix into consideration everywhere) # Logos diff --git a/src/main.lua b/src/main.lua index efa9d22..5637107 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,24 +1,3 @@ ---[[ -TODO: - - document REST API (mention rq IDs and endpoint information, list endpoints+args+CRUD type, unknown values are empty fields) - (describe fail/error difference: fail is valid rq..could not comply, while error is invalid rq _or_ system error) - - 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) - (!!!is this true? it could very well be caused by a uhttpd restart) - - 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 netconf 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: - - using iwinfo with interface name 'radio0' yields very little 'info' output while wlan0 works fine. - However, sometimes wlan0 disappears (happened after trying to associate with non-existing network)...why? -]]-- - local l = require("logger") local RequestClass = require("rest.request") local ResponseClass = require("rest.response") @@ -61,38 +40,6 @@ local function init() return true end ---usually returns function+nil, function+number in case of number in place of function name; or ---nil+string if given arguments could not be resolved -local function resolveApiFunction(mod, func) - if mod == nil then return nil, ("missing module name in CGI call") end - - local ok, mObj - local reqModPath = "rest.api.api_" .. mod - - if DEBUG_PCALLS then ok, mObj = true, require(reqModPath) - else ok, mObj = pcall(require, reqModPath) - end - - if ok == false then return nil, ("API module '" .. mod .. "' does not exist") end - - if mObj == nil then return nil, ("API module '" .. mod .. "' could not be found") end - - if mObj.isApi ~= true then return nil, ("module '" .. mod .. "' is not part of the CGI API") end - - if (func == nil or func == '') then func = "_global" end --treat empty function name as nil - local f = mObj[func] - - if (type(f) ~= "function") then - if tonumber(func) ~= nil then - return mObj["_global"], tonumber(func) - else - return nil, ("function '" .. func .. "' does not exist in API module '" .. mod .. "'") - end - end - - return f -end - local function main() local rq = RequestClass.new(postData, DEBUG_PCALLS) -- initializes itself using various environment variables and the arg array @@ -111,35 +58,10 @@ end else io.write ("Content-type: text/plain\r\n\r\n") + local response, err = rq:handle() - local mod = rq:getApiModule() - local func = rq:getApiFunction() - - local sf,sr = resolveApiFunction(mod, func) - if (sf ~= nil) then - if (sr ~= nil) then - rq:setBlankArgument(sr) - end - - local ok, r - if DEBUG_PCALLS then ok, r = true, sf(rq) - else ok, r = pcall(sf, rq) - 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 - 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 + if err ~= nil then l:error(err) end + response:send() end end @@ -147,7 +69,7 @@ local s, msg = init() if s == false then local resp = ResponseClass.new() resp:setError("initialization failed (" .. msg .. ")") - print(resp:serializeAsJson()) --FIXME: this message does not seem to be sent + resp:send() --FIXME: this message does not seem to be sent l:error("initialization failed (" .. msg .. ")") --NOTE: this assumes the logger has been inited properly, despite init() having failed os.exit(1) else diff --git a/src/rest/api/api_test.lua b/src/rest/api/api_test.lua index f5bceeb..242bff0 100644 --- a/src/rest/api/api_test.lua +++ b/src/rest/api/api_test.lua @@ -17,19 +17,22 @@ end function M.success(d) local r = ResponseClass.new(d) - r:setSuccess() + r:setSuccess("this successful response has been generated on purpose") + r:addData("url", "http://xkcd.com/349/") return r end function M.fail(d) local r = ResponseClass.new(d) - r:setFail() + r:setFail("this failure has been generated on purpose") + r:addData("url", "http://xkcd.com/336/") return r end function M.error(d) local r = ResponseClass.new(d) r:setError("this error has been generated on purpose") + r:addData("url", "http://xkcd.com/1024/") return r end diff --git a/src/rest/request.lua b/src/rest/request.lua index e5f61a3..86b2a6a 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -1,8 +1,11 @@ local urlcode = require("util.urlcode") +local ResponseClass = require("rest.response") local M = {} M.__index = M +local GLOBAL_API_FUNCTION_NAME = "_global" + local function kvTableFromUrlEncodedString(encodedText) local args = {} if (encodedText ~= nil) then @@ -113,4 +116,75 @@ function M:getAll() end end +--returns either a module object, or nil+errmsg +local function resolveApiModule(modname) + if modname == nil then return nil, "missing module name" end + if string.find(modname, "_") == 1 then return nil, "module names starting with '_' are preserved for internal use" end + + local reqModName = "rest.api.api_" .. modname + local ok, modObj + + --TODO: create config.lua which contains DEBUG_PCALLS (nothing else for the moment) + if DEBUG_PCALLS then ok, modObj = true, require(reqModName) + else ok, modObj = pcall(require, reqModName) + end + + if ok == false then return nil, "API module does not exist" end + if modObj == nil then return nil, "API module could not be found" end + if modObj.isApi ~= true then return nil, "module is not part of the CGI API" end + + return modObj +end + +--returns funcobj+nil (usual), funcobj+number (global func with blank arg), or nil+errmsg (unresolvable or inaccessible) +local function resolveApiFunction(modname, funcname) + if funcname and string.find(funcname, "_") == 1 then return nil, "function names starting with '_' are preserved for internal use" end + + local mod, msg = resolveApiModule(modname) + + if (funcname == nil or funcname == '') then funcname = GLOBAL_API_FUNCTION_NAME end --treat empty function name as nil + local f = mod[funcname] + local funcNumber = tonumber(funcname) + + if (type(f) == "function") then + return f + elseif funcNumber ~= nil then + return mod[GLOBAL_API_FUNCTION_NAME], funcNumber + else + return nil, ("function '" .. funcname .. "' does not exist in API module '" .. modname .. "'") + end +end + +--returns either a response object+nil, or response object+errmsg +function M:handle() + + --TEMP: should be moved to init + local mod = self:getApiModule() + local func = self:getApiFunction() + local sf, sr = resolveApiFunction(mod, func) + + local resp = ResponseClass.new(rq) --TEMP: do not do this before resolving. after resolving has been moved to init that will be automatically true + + if (sf ~= nil) then + if (sr ~= nil) then self:setBlankArgument(sr) end + + local ok, r + if DEBUG_PCALLS then ok, r = true, sf(self) + else ok, r = pcall(sf, self) + end + + if ok == true then + return r, nil + else + resp:setError("call to function '" .. mod .. "/" .. sr .. "' failed") + return resp, ("calling function '" .. func .. "' in API module '" .. mod .. "' somehow failed ('" .. r .. "')") + end + else + resp:setError("function unknown '" .. (mod or "") .. "/" .. (func or "") .. "'") + return resp, ("could not resolve requested API function ('" .. sr .. "')") + end + + return resp +end + return M diff --git a/src/rest/response.lua b/src/rest/response.lua index cb846d5..0f1f564 100644 --- a/src/rest/response.lua +++ b/src/rest/response.lua @@ -61,4 +61,8 @@ function M:serializeAsJson() return JSON:encode(self.body) end +function M:send() + print(self:serializeAsJson()) +end + return M