Change REST API to obtain module/function arguments from URL path; move several network settings to config.h.

This commit is contained in:
Wouter R 2013-07-11 10:30:59 +02:00
parent 1ac3f130ff
commit 9dd2928755
8 changed files with 87 additions and 35 deletions

20
TODO.md
View File

@ -4,6 +4,7 @@
* in 'test' dir next to 'src', with API tests under 'test/www/'
* www tests check functionality of the test module
* www tests also provide an interface to run arbitrary get/post requests
* test path splitting as well
- document REST API
* fail/error difference: fail is a valid rq aka 'could not comply', while error is invalid rq _or_ system error
* modules/functions prefixed with '_' are for internal use
@ -11,7 +12,7 @@
* list endpoints+args+CRUD type
* success/fail/error statuses are justified by drupal api
* unknown values (e.g. in network info) are either empty or unmentioned fields
- use a slightly more descriptive success/error definition (e.g. errortype=system/missing-arg/generic)
- define a list of REST error codes to be more descriptive for clients (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)
@ -21,25 +22,24 @@
* 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 (set once on installation and then only upon explicit request? (e.g. api/config/wifiname/default))
- 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'
* use a uci wifibox config to store configuration and a uci wifibox-defaults config as fallback-lookup (which contains a complete default configuration)
* specify min/max/type/regex for each config key in separate lua file
* perhaps defaults should be specified together with min/max/type/regex
- dynamic AP name based on partial MAC (present in default config so it can be overridden and reverted again)
- require api functions which change state to be invoked as post request
* can this be modelled like java annotations or c function attributes?
* otherwise maybe pair each function with <func>_attribs = {…}?
- add API functions to test network connectivity in steps (any chance(e.g. ~ap)? ifup? hasip? resolve? ping?) to network or test
- handling requests which need a restart of uhttpd (e.g. network/openap) will probably respond with some kind of 'please check back in a few seconds' response
- 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>
# 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
- 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)
- 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 system api module? for check-updates/do-update/etc
- 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>)
- (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'

View File

@ -7,4 +7,8 @@ M.DEBUG_PCALLS = true
--REST responses will contain 'module' and 'function' keys describing what was requested
M.API_INCLUDE_ENDPOINT_INFO = true
M.DEFAULT_AP_SSID = "d3d-ap-%MAC_ADDR_TAIL%"
M.DEFAULT_AP_ADDRESS = "192.168.10.1"
M.DEFAULT_AP_NETMASK = "255.255.255.0"
return M

View File

@ -1,3 +1,4 @@
local config = require("config")
local u = require("util.utils")
local l = require("logger")
local uci = require("uci").cursor()
@ -96,13 +97,13 @@ function reconf.apnet_add_noreload(dirtyList) reconf.apnet_add(dirtyList, true)
function reconf.apnet_add(dirtyList, noReload)
local sname = nil
uci:foreach("wireless", "wifi-iface", function(s)
if s.ssid == wifi.AP_SSID then sname = s[".name"]; return false end
if s.ssid == config.DEFAULT_AP_SSID then sname = s[".name"]; return false end
end)
if sname == nil then sname = uci:add("wireless", "wifi-iface") end
M.uciTableSet("wireless", sname, {
network = wifi.NET,
ssid = wifi.AP_SSID,
ssid = config.DEFAULT_AP_SSID,
encryption = "none",
device = "radio0",
mode = "ap",
@ -114,7 +115,7 @@ end
function reconf.apnet_rm(dirtyList)
local sname = nil
uci:foreach("wireless", "wifi-iface", function(s)
if s.ssid == wifi.AP_SSID then sname = s[".name"]; return false end
if s.ssid == config.DEFAULT_AP_SSID then sname = s[".name"]; return false end
end)
if sname == nil then return l:info("AP network configuration does not exist, nothing to remove") end
uci:delete("wireless", sname)
@ -129,8 +130,8 @@ function reconf.staticaddr_add(dirtyList)
--NOTE: 'type = "bridge"' should -not- be added as this prevents defining a separate dhcp pool (http://wiki.openwrt.org/doc/recipes/routedap)
M.uciTableSet("network", wifi.NET, {
proto = "static",
ipaddr = wifi.AP_ADDRESS,
netmask = wifi.AP_NETMASK
ipaddr = config.DEFAULT_AP_ADDRESS,
netmask = config.DEFAULT_AP_NETMASK
})
bothBits(dirtyList, "network")
end
@ -177,7 +178,7 @@ end
--[[ Add/remove redirecton of all DNS requests to self ]]
function reconf.dnsredir_add(dirtyList)
local redirText = "/#/" .. wifi.AP_ADDRESS
local redirText = "/#/" .. config.DEFAULT_AP_ADDRESS
local sname = u.getUciSectionName("dhcp", "dnsmasq")
if sname == nil then return l:error("dhcp config does not contain a dnsmasq section") end
if uci:get("dhcp", sname, "address") ~= nil then return l:debug("DNS address redirection already in place, not re-adding", false) end
@ -225,7 +226,7 @@ function reconf.natreflect_add(dirtyList)
proto = "tcp",
src_dport = "80",
dest_port = "80",
dest_ip = wifi.AP_ADDRESS,
dest_ip = config.DEFAULT_AP_ADDRESS,
target = "DNAT"
})
bothBits(dirtyList, "firewall")

View File

@ -8,9 +8,6 @@ local M = {}
--NOTE: fallback device 'radio0' is required because sometimes the wlan0 device disappears
M.DFL_DEVICE = "wlan0"
M.DFL_DEVICE_FALLBACK = "radio0"
M.AP_SSID = "d3d-ap"
M.AP_ADDRESS = "192.168.10.1"
M.AP_NETMASK = "255.255.255.0"
M.NET = "wlan"
local dev, dev_api
@ -82,6 +79,21 @@ function M.getDeviceState()
return result
end
--returns the wireless device's MAC address (as string, without colons)
--(lua numbers on openWrt seem to be 32bit so they cannot represent a MAC address as one number)
function M.getMacAddress()
local iw = iwinfo[dev_api]
local macText = iw.bssid(dev)
local out = ""
for i = 0, 5 do
local bt = string.sub(macText, i*3+1, i*3+2)
out = out .. bt
end
return out
end
--- Return one or all available wifi networks resulting from an iwinfo scan
-- @param ssid return data for given SSID or for all networks if SSID not given
-- @return data for all or requested network; false+error on failure or nil when requested network not found

View File

@ -1,3 +1,4 @@
local config = require("config")
local l = require("logger")
local u = require("util.utils")
local netconf = require("network.netconfig")
@ -24,7 +25,7 @@ function M.available(request, response)
response:setSuccess("")
local netInfoList = {}
for _, se in ipairs(sr) do
if noFilter or se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then
if noFilter or se.mode ~= "ap" and se.ssid ~= config.DEFAULT_AP_SSID then
local netInfo = {}
netInfo["ssid"] = se.ssid
@ -139,10 +140,10 @@ end
function M.openap(request, response)
--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)
wifi.activateConfig(config.DEFAULT_AP_SSID)
netconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" }
response:setSuccess("switched to Access Point mode")
response:addData("ssid", wifi.AP_SSID)
response:addData("ssid", config.DEFAULT_AP_SSID)
end
--UNTESTED

View File

@ -29,7 +29,9 @@ end
function M.echo(request, response)
response:setSuccess("request echo")
response:addData("request_data", request)
response:addData("request_data", request:getAll())
response:addData("blank_argument", request:getBlankArgument())
response:addData("path_data", request:getPathData())
end
return M

View File

@ -1,3 +1,4 @@
local util = require("util.utils") --required for string:split()
local urlcode = require("util.urlcode")
local config = require("config")
local ResponseClass = require("rest.response")
@ -42,6 +43,12 @@ local function kvTableFromArray(argArray)
return args
end
--NOTE: this function ignores empty tokens (e.g. '/a//b/' yields { [1] = a, [2] = b })
local function arrayFromPath(pathText)
return pathText and pathText:split("/") or {} --FIXME: nothing returned? regardless of which sep is used
--return pathText:split("/")
end
--returns either a module object, or nil+errmsg
local function resolveApiModule(modname)
@ -68,6 +75,10 @@ local function resolveApiFunction(modname, funcname)
local mod, msg = resolveApiModule(modname)
if mod == nil then
return nil, msg
end
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)
@ -106,22 +117,27 @@ function M.new(postData, debug)
self.cmdLineArgs = kvTableFromArray(arg)
self.getArgs = kvTableFromUrlEncodedString(os.getenv("QUERY_STRING"))
self.postArgs = kvTableFromUrlEncodedString(postData)
self.pathArgs = arrayFromPath(os.getenv("PATH_INFO"))
--TEMP: until these can be extracted from the url path itself
self.requestedApiModule = self.getArgs["m"]
self.requestedApiFunction = self.getArgs["f"]
if debug then
self.requestedApiModule = self.cmdLineArgs["m"] or self.requestedApiModule
self.requestedApiFunction = self.cmdLineArgs["f"] or self.requestedApiFunction
--override path arguments with command line parameter if debugging is enabled
if debug and self.requestMethod == "CMDLINE" then
self.pathArgs = arrayFromPath(self.cmdLineArgs["p"])
end
if #self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs[1] end
if #self.pathArgs >= 2 then self.requestedApiFunction = self.pathArgs[2] end
-- if debug then
-- self.requestedApiModule = self.cmdLineArgs["m"] or self.requestedApiModule
-- self.requestedApiFunction = self.cmdLineArgs["f"] or self.requestedApiFunction
-- end
if self.requestedApiModule == "" then self.requestedApiModule = nil end
if self.requestedApiFunction == "" then self.requestedApiFunction = nil end
-- Perform module/function resolution
--TODO: improve naming and perhaps argument passing
local sfunc, sres = resolveApiFunction(self:getRequestedApiModule(), self:getRequestedApiFunction())
if sfunc ~= nil then --function (possibly the global one) could be resolved
@ -130,7 +146,12 @@ function M.new(postData, debug)
self:setBlankArgument(sres)
self.realApiFunctionName = GLOBAL_API_FUNCTION_NAME
else --resolved without blank argument but still potentially the global function, hence the _or_ construction
self.realApiFunctionName = self:getRequestedApiFunction() or GLOBAL_API_FUNCTION_NAME
if self:getRequestedApiFunction() ~= nil then
self.realApiFunctionName = self:getRequestedApiFunction()
if #self.pathArgs >= 3 then self:setBlankArgument(self.pathArgs[3]) end --aha, we have both a function and a blank argument
else
self.realApiFunctionName = GLOBAL_API_FUNCTION_NAME
end
end
else
--instead of throwing an error, save the message for handle() which is expected to return a response anyway
@ -141,7 +162,7 @@ function M.new(postData, debug)
return self
end
--GET/POST/CMDLINE
--returns either GET or POST or CMDLINE
function M:getRequestMethod()
return self.requestMethod
end
@ -194,6 +215,10 @@ function M:getAll()
end
end
function M:getPathData()
return self.pathArgs
end
--returns either a response object+nil, or response object+errmsg
function M:handle()
@ -215,7 +240,7 @@ function M:handle()
return resp, ("calling function '" .. self.realApiFunctionName .. "' in API module '" .. modname .. "' somehow failed ('" .. r .. "')")
end
else
resp:setError("function unknown '" .. (modname or "<empty>") .. "/" .. (self:getRequestedApiFunction() or "<empty>") .. "'")
resp:setError("function or module unknown '" .. (modname or "<empty>") .. "/" .. (self:getRequestedApiFunction() or "<empty>") .. "'")
return resp, ("could not resolve requested API function ('" .. self.resolutionError .. "')")
end

View File

@ -2,6 +2,13 @@ local uci = require("uci").cursor()
local M = {}
function string:split(sep)
local sep, fields = sep or ":", {}
local pattern = string.format("([^%s]+)", sep)
self:gsub(pattern, function(c) fields[#fields+1] = c end)
return fields
end
function M.toboolean(s)
if not s then return false end