0
0
mirror of https://github.com/Doodle3D/doodle3d-firmware.git synced 2024-12-22 02:53:49 +01:00

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/' * in 'test' dir next to 'src', with API tests under 'test/www/'
* www tests check functionality of the test module * www tests check functionality of the test module
* www tests also provide an interface to run arbitrary get/post requests * www tests also provide an interface to run arbitrary get/post requests
* test path splitting as well
- document REST API - document REST API
* fail/error difference: fail is a valid rq aka 'could not comply', while error is invalid rq _or_ system error * 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 * modules/functions prefixed with '_' are for internal use
@ -11,7 +12,7 @@
* list endpoints+args+CRUD type * list endpoints+args+CRUD type
* success/fail/error statuses are justified by drupal api * success/fail/error statuses are justified by drupal api
* unknown values (e.g. in network info) are either empty or unmentioned fields * 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 - steps to take regarding versioning/updating
* versioning scheme * versioning scheme
* create feed location (e.g. www.doodle3d.com/firmware/packages) (see here: http://wiki.openwrt.org/doc/packages#third.party.packages) * 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' * 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?) * 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 * 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 - require api functions which change state to be invoked as post request
* can this be modelled like java annotations or c function attributes? * can this be modelled like java annotations or c function attributes?
* otherwise maybe pair each function with <func>_attribs = {…}? * 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 - 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: - 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)} reconf.WWW_RENAME_NAME, wifihelper.{AP_ADDRESS, AP_NETMASK, (NET)}
<https://github.com/2ion/ini.lua> <https://github.com/2ion/ini.lua>
# Ideas / issues to work out # 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? for check-updates/do-update/etc
- add system api module? with 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>)
- 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>
- (this is an old todo item from network:available(), might still be relevant at some point) - (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 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' 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 --REST responses will contain 'module' and 'function' keys describing what was requested
M.API_INCLUDE_ENDPOINT_INFO = true 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 return M

View File

@ -1,3 +1,4 @@
local config = require("config")
local u = require("util.utils") local u = require("util.utils")
local l = require("logger") local l = require("logger")
local uci = require("uci").cursor() 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) function reconf.apnet_add(dirtyList, noReload)
local sname = nil local sname = nil
uci:foreach("wireless", "wifi-iface", function(s) 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) end)
if sname == nil then sname = uci:add("wireless", "wifi-iface") end if sname == nil then sname = uci:add("wireless", "wifi-iface") end
M.uciTableSet("wireless", sname, { M.uciTableSet("wireless", sname, {
network = wifi.NET, network = wifi.NET,
ssid = wifi.AP_SSID, ssid = config.DEFAULT_AP_SSID,
encryption = "none", encryption = "none",
device = "radio0", device = "radio0",
mode = "ap", mode = "ap",
@ -114,7 +115,7 @@ end
function reconf.apnet_rm(dirtyList) function reconf.apnet_rm(dirtyList)
local sname = nil local sname = nil
uci:foreach("wireless", "wifi-iface", function(s) 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) end)
if sname == nil then return l:info("AP network configuration does not exist, nothing to remove") end if sname == nil then return l:info("AP network configuration does not exist, nothing to remove") end
uci:delete("wireless", sname) 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) --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, { M.uciTableSet("network", wifi.NET, {
proto = "static", proto = "static",
ipaddr = wifi.AP_ADDRESS, ipaddr = config.DEFAULT_AP_ADDRESS,
netmask = wifi.AP_NETMASK netmask = config.DEFAULT_AP_NETMASK
}) })
bothBits(dirtyList, "network") bothBits(dirtyList, "network")
end end
@ -177,7 +178,7 @@ end
--[[ Add/remove redirecton of all DNS requests to self ]] --[[ Add/remove redirecton of all DNS requests to self ]]
function reconf.dnsredir_add(dirtyList) function reconf.dnsredir_add(dirtyList)
local redirText = "/#/" .. wifi.AP_ADDRESS local redirText = "/#/" .. config.DEFAULT_AP_ADDRESS
local sname = u.getUciSectionName("dhcp", "dnsmasq") local sname = u.getUciSectionName("dhcp", "dnsmasq")
if sname == nil then return l:error("dhcp config does not contain a dnsmasq section") end 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 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", proto = "tcp",
src_dport = "80", src_dport = "80",
dest_port = "80", dest_port = "80",
dest_ip = wifi.AP_ADDRESS, dest_ip = config.DEFAULT_AP_ADDRESS,
target = "DNAT" target = "DNAT"
}) })
bothBits(dirtyList, "firewall") bothBits(dirtyList, "firewall")

View File

@ -8,9 +8,6 @@ local M = {}
--NOTE: fallback device 'radio0' is required because sometimes the wlan0 device disappears --NOTE: fallback device 'radio0' is required because sometimes the wlan0 device disappears
M.DFL_DEVICE = "wlan0" M.DFL_DEVICE = "wlan0"
M.DFL_DEVICE_FALLBACK = "radio0" 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" M.NET = "wlan"
local dev, dev_api local dev, dev_api
@ -82,6 +79,21 @@ function M.getDeviceState()
return result return result
end 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 --- 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 -- @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 -- @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 l = require("logger")
local u = require("util.utils") local u = require("util.utils")
local netconf = require("network.netconfig") local netconf = require("network.netconfig")
@ -24,7 +25,7 @@ function M.available(request, response)
response:setSuccess("") response:setSuccess("")
local netInfoList = {} local netInfoList = {}
for _, se in ipairs(sr) do 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 = {} local netInfo = {}
netInfo["ssid"] = se.ssid netInfo["ssid"] = se.ssid
@ -139,10 +140,10 @@ end
function M.openap(request, response) 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 --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"} 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" } netconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" }
response:setSuccess("switched to Access Point mode") response:setSuccess("switched to Access Point mode")
response:addData("ssid", wifi.AP_SSID) response:addData("ssid", config.DEFAULT_AP_SSID)
end end
--UNTESTED --UNTESTED

View File

@ -29,7 +29,9 @@ end
function M.echo(request, response) function M.echo(request, response)
response:setSuccess("request echo") 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 end
return M return M

View File

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

View File

@ -2,6 +2,13 @@ local uci = require("uci").cursor()
local M = {} 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) function M.toboolean(s)
if not s then return false end if not s then return false end