From 9dd29287550a6f05bd7ecd011176eea685008200 Mon Sep 17 00:00:00 2001 From: Wouter R Date: Thu, 11 Jul 2013 10:30:59 +0200 Subject: [PATCH] Change REST API to obtain module/function arguments from URL path; move several network settings to config.h. --- TODO.md | 20 +++++++-------- src/config.lua | 4 +++ src/network/netconfig.lua | 15 ++++++------ src/network/wlanconfig.lua | 18 +++++++++++--- src/rest/api/api_network.lua | 7 +++--- src/rest/api/api_test.lua | 4 ++- src/rest/request.lua | 47 +++++++++++++++++++++++++++--------- src/util/utils.lua | 7 ++++++ 8 files changed, 87 insertions(+), 35 deletions(-) diff --git a/TODO.md b/TODO.md index dc8fdbd..7b1d8c7 100644 --- a/TODO.md +++ b/TODO.md @@ -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 _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)} # 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 - + - add system api module? for check-updates/do-update/etc + - 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' diff --git a/src/config.lua b/src/config.lua index a9cdc28..9bb3e6e 100644 --- a/src/config.lua +++ b/src/config.lua @@ -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 diff --git a/src/network/netconfig.lua b/src/network/netconfig.lua index b5f775f..c350638 100644 --- a/src/network/netconfig.lua +++ b/src/network/netconfig.lua @@ -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") diff --git a/src/network/wlanconfig.lua b/src/network/wlanconfig.lua index 67dfe2a..46ace29 100644 --- a/src/network/wlanconfig.lua +++ b/src/network/wlanconfig.lua @@ -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 diff --git a/src/rest/api/api_network.lua b/src/rest/api/api_network.lua index 5360a3f..aee773b 100644 --- a/src/rest/api/api_network.lua +++ b/src/rest/api/api_network.lua @@ -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 diff --git a/src/rest/api/api_test.lua b/src/rest/api/api_test.lua index 840e49c..57844d8 100644 --- a/src/rest/api/api_test.lua +++ b/src/rest/api/api_test.lua @@ -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 diff --git a/src/rest/request.lua b/src/rest/request.lua index c55d0b5..b988757 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -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 "") .. "/" .. (self:getRequestedApiFunction() or "") .. "'") + resp:setError("function or module unknown '" .. (modname or "") .. "/" .. (self:getRequestedApiFunction() or "") .. "'") return resp, ("could not resolve requested API function ('" .. self.resolutionError .. "')") end diff --git a/src/util/utils.lua b/src/util/utils.lua index f315384..6ddb119 100644 --- a/src/util/utils.lua +++ b/src/util/utils.lua @@ -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