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

Change split function so it also returns empty fields; add API access control; support returning http status codes; changes towards support for running as uhttp embedded lua module.

This commit is contained in:
Wouter R 2013-07-17 08:06:04 +02:00
parent 80fdadde47
commit 3726063b99
6 changed files with 85 additions and 52 deletions

View File

@ -5,7 +5,7 @@ local M = {}
M.DEBUG_PCALLS = true 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 = false
M.DEFAULT_AP_SSID = "d3d-ap-%MAC_ADDR_TAIL%" M.DEFAULT_AP_SSID = "d3d-ap-%MAC_ADDR_TAIL%"
M.DEFAULT_AP_ADDRESS = "192.168.10.1" M.DEFAULT_AP_ADDRESS = "192.168.10.1"

View File

@ -1,3 +1,5 @@
package.path = package.path .. ';/usr/share/lua/wifibox/?.lua'
local l = require("logger") local l = require("logger")
local RequestClass = require("rest.request") local RequestClass = require("rest.request")
local ResponseClass = require("rest.response") local ResponseClass = require("rest.response")
@ -38,7 +40,8 @@ end
local function main() local function main()
local rq = RequestClass.new(postData, config.DEBUG_PCALLS) local rq = RequestClass.new(postData, config.DEBUG_PCALLS)
l:info("received request of type " .. rq:getRequestMethod() .. " with arguments: " .. l:dump(rq:getAll())) l:info("received request of type " .. rq:getRequestMethod() .. " for " .. (rq:getRequestedApiModule() or "<unknown>")
.. "/" .. (rq:getRealApiFunctionName() or "<unknown>") .. " with arguments: " .. l:dump(rq:getAll()))
if rq:getRequestMethod() ~= "CMDLINE" then if rq:getRequestMethod() ~= "CMDLINE" then
l:info("remote IP/port: " .. rq:getRemoteHost() .. "/" .. rq:getRemotePort()) l:info("remote IP/port: " .. rq:getRemoteHost() .. "/" .. rq:getRemotePort())
l:debug("user agent: " .. rq:getUserAgent()) l:debug("user agent: " .. rq:getUserAgent())
@ -52,7 +55,6 @@ end
end end
else else
io.write ("Content-type: text/plain\r\n\r\n")
local response, err = rq:handle() local response, err = rq:handle()
if err ~= nil then l:error(err) end if err ~= nil then l:error(err) end

View File

@ -5,6 +5,15 @@ local M = {}
M.isApi = true M.isApi = true
--empty or nil is equivalent to 'ANY', otherwise restrict to specified letters (command-line is always allowed)
M._access = {
_global = "GET",
success = "GET", fail = "GET", error = "GET",
read = "GET", write = "POST", readwrite = "ANY", readwrite2 = "",
echo = "GET"
}
function M._global(request, response) function M._global(request, response)
local ba = request:getBlankArgument() local ba = request:getBlankArgument()
@ -27,6 +36,13 @@ function M.error(request, response)
response:addData("url", "http://xkcd.com/1024/") response:addData("url", "http://xkcd.com/1024/")
end end
function M.read(request, response) response:setSuccess("this endpoint can only be accessed through GET request") end
function M.write(request, response) response:setSuccess("this endpoint can only be accessed through POST request") end
function M.readwrite(request, response) response:setSuccess("this endpoint can only be accessed through POST request") end
function M.readwrite2(request, response) response:setSuccess("this endpoint can only be accessed through POST request") end
function M.echo(request, response) function M.echo(request, response)
response:setSuccess("request echo") response:setSuccess("request echo")
response:addData("request_data", request:getAll()) response:addData("request_data", request:getAll())

View File

@ -17,6 +17,7 @@ M.requestedApiFunction = nil
M.resolvedApiFunction = nil --will contain function address, or nil M.resolvedApiFunction = nil --will contain function address, or nil
M.realApiFunctionName = nil --will contain requested name, or global name, or nil M.realApiFunctionName = nil --will contain requested name, or global name, or nil
M.resolutionError = nil --non-nil means function could not be resolved M.resolutionError = nil --non-nil means function could not be resolved
M.moduleAccessTable = nil
local function kvTableFromUrlEncodedString(encodedText) local function kvTableFromUrlEncodedString(encodedText)
@ -31,6 +32,8 @@ end
local function kvTableFromArray(argArray) local function kvTableFromArray(argArray)
local args = {} local args = {}
if not argArray then return args end
for _, v in ipairs(argArray) do for _, v in ipairs(argArray) do
local split = v:find("=") local split = v:find("=")
if split ~= nil then if split ~= nil then
@ -45,8 +48,12 @@ end
--NOTE: this function ignores empty tokens (e.g. '/a//b/' yields { [1] = a, [2] = b }) --NOTE: this function ignores empty tokens (e.g. '/a//b/' yields { [1] = a, [2] = b })
local function arrayFromPath(pathText) local function arrayFromPath(pathText)
return pathText and pathText:split("/") or {} --FIXME: nothing returned? regardless of which sep is used return pathText and pathText:split("/") or {}
--return pathText:split("/") end
--returns true if acceptable is nil or empty or 'ANY' or if it contains requested
local function matchRequestMethod(acceptable, requested)
return acceptable == nil or acceptable == '' or acceptable == 'ANY' or string.find(acceptable, requested)
end end
@ -69,8 +76,11 @@ local function resolveApiModule(modname)
return modObj return modObj
end end
--returns funcobj+nil (usual), funcobj+number (global func with blank arg), or nil+errmsg (unresolvable or inaccessible) --returns resultData+nil (usual), or nil+errmsg (unresolvable or inaccessible)
--resultData contains 'func', 'accessTable' and if found, also 'blankArg'
local function resolveApiFunction(modname, funcname) local function resolveApiFunction(modname, funcname)
local resultData = {}
if funcname and string.find(funcname, "_") == 1 then return nil, "function names starting with '_' are preserved for internal use" end 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) local mod, msg = resolveApiModule(modname)
@ -84,12 +94,17 @@ local function resolveApiFunction(modname, funcname)
local funcNumber = tonumber(funcname) local funcNumber = tonumber(funcname)
if (type(f) == "function") then if (type(f) == "function") then
return f resultData.func = f
resultData.accessTable = mod._access
elseif funcNumber ~= nil then elseif funcNumber ~= nil then
return mod[GLOBAL_API_FUNCTION_NAME], funcNumber resultData.func = mod[GLOBAL_API_FUNCTION_NAME]
resultData.accessTable = mod._access
resultData.blankArg = funcNumber
else else
return nil, ("function '" .. funcname .. "' does not exist in API module '" .. modname .. "'") return nil, ("function '" .. funcname .. "' does not exist in API module '" .. modname .. "'")
end end
return resultData
end end
@ -123,27 +138,24 @@ function M.new(postData, debug)
if debug and self.requestMethod == "CMDLINE" then if debug and self.requestMethod == "CMDLINE" then
self.pathArgs = arrayFromPath(self.cmdLineArgs["p"]) self.pathArgs = arrayFromPath(self.cmdLineArgs["p"])
end end
table.remove(self.pathArgs, 1) --drop the first 'empty' field caused by the opening slash of the query string
if #self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs[1] end if #self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs[1] end
if #self.pathArgs >= 2 then self.requestedApiFunction = self.pathArgs[2] 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
local sfunc, sres = resolveApiFunction(self:getRequestedApiModule(), self:getRequestedApiFunction()) local rData, errMsg = resolveApiFunction(self:getRequestedApiModule(), self:getRequestedApiFunction())
if sfunc ~= nil then --function (possibly the global one) could be resolved if rData ~= nil and rData.func ~= nil then --function (possibly the global one) could be resolved
self.resolvedApiFunction = sfunc self.resolvedApiFunction = rData.func
if sres ~= nil then --apparently it was the global one, and we received a 'blank argument' self.moduleAccessTable = rData.accessTable
self:setBlankArgument(sres) if rData.blankArg ~= nil then --apparently it was the global one, and we received a 'blank argument'
self:setBlankArgument(rData.blankArg)
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
if self:getRequestedApiFunction() ~= nil then if self:getRequestedApiFunction() ~= nil then
@ -155,38 +167,19 @@ function M.new(postData, debug)
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
self.resolutionError = sres self.resolutionError = errMsg
end end
return self return self
end end
--returns either GET or POST or CMDLINE function M:getRequestMethod() return self.requestMethod end --returns either GET or POST or CMDLINE
function M:getRequestMethod() function M:getRequestedApiModule() return self.requestedApiModule end
return self.requestMethod function M:getRequestedApiFunction() return self.requestedApiFunction end
end function M:getRealApiFunctionName() return self.realApiFunctionName end
function M:getBlankArgument() return self.blankArgument end
function M:getRequestedApiModule() function M:setBlankArgument(arg) self.blankArgument = arg end
return self.requestedApiModule
end
function M:getRequestedApiFunction()
return self.requestedApiFunction
end
function M:getRealApiFunctionName()
return self.realApiFunctionName
end
function M:getBlankArgument()
return self.blankArgument
end
function M:setBlankArgument(arg)
self.blankArgument = arg
end
function M:getRemoteHost() return self.remoteHost or "" end function M:getRemoteHost() return self.remoteHost or "" end
function M:getRemotePort() return self.remotePort or 0 end function M:getRemotePort() return self.remotePort or 0 end
function M:getUserAgent() return self.userAgent or "" end function M:getUserAgent() return self.userAgent or "" end
@ -219,13 +212,19 @@ function M:getPathData()
return self.pathArgs return self.pathArgs
end 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()
local modname = self:getRequestedApiModule() local modname = self:getRequestedApiModule()
local resp = ResponseClass.new(self) local resp = ResponseClass.new(self)
if (self.resolvedApiFunction ~= nil) then --we found a function (possible the global function) if (self.resolvedApiFunction ~= nil) then --we found a function (possible the global function)
--check access type
local accessText = self.moduleAccessTable[self.realApiFunctionName]
if not matchRequestMethod(accessText, self.requestMethod) then
resp:setError("function '" .. modname .. "/" .. self.realApiFunctionName .. "' requires different request method ('" .. accessText .. "')")
return resp, "incorrect access method (" .. accessText .. " != " .. self.requestMethod .. ")"
end
--invoke the function --invoke the function
local ok, r local ok, r
if config.DEBUG_PCALLS then ok, r = true, self.resolvedApiFunction(self, resp) if config.DEBUG_PCALLS then ok, r = true, self.resolvedApiFunction(self, resp)

View File

@ -1,4 +1,4 @@
local JSON = (loadfile "util/JSON.lua")() local JSON = require("util/JSON")
local config = require("config") local config = require("config")
local M = {} local M = {}
@ -6,6 +6,9 @@ M.__index = M
local REQUEST_ID_ARGUMENT = "rq_id" local REQUEST_ID_ARGUMENT = "rq_id"
M.httpStatusCode, M.httpStatusText = nil, nil
setmetatable(M, { setmetatable(M, {
__call = function(cls, ...) __call = function(cls, ...)
return cls.new(...) return cls.new(...)
@ -16,7 +19,8 @@ setmetatable(M, {
function M.new(requestObject) function M.new(requestObject)
local self = setmetatable({}, M) local self = setmetatable({}, M)
self.body = {status = nil, data = {}} self.body = { status = nil, data = {} }
self:setHttpStatus(200, "OK")
if requestObject ~= nil then if requestObject ~= nil then
local rqId = requestObject:get(REQUEST_ID_ARGUMENT) local rqId = requestObject:get(REQUEST_ID_ARGUMENT)
@ -31,6 +35,11 @@ function M.new(requestObject)
return self return self
end end
function M:setHttpStatus(code, text)
if code ~= nil then self.httpStatusCode = code end
if text ~= nil then self.httpStatusText = text end
end
function M:setSuccess(msg) function M:setSuccess(msg)
self.body.status = "success" self.body.status = "success"
if msg ~= "" then self.body.msg = msg end if msg ~= "" then self.body.msg = msg end
@ -44,6 +53,8 @@ end
function M:setError(msg) function M:setError(msg)
self.body.status = "error" self.body.status = "error"
if msg ~= "" then self.body.msg = msg end if msg ~= "" then self.body.msg = msg end
self:addData("more_info", "http://doodle3d.nl/wiki/wiki/communication-api")
end end
--NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key --NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key
@ -57,6 +68,8 @@ function M:serializeAsJson()
end end
function M:send() function M:send()
io.write("Status: " .. self.httpStatusCode .. " " .. self.httpStatusText .. "\r\n")
io.write ("Content-type: text/plain\r\n\r\n")
print(self:serializeAsJson()) print(self:serializeAsJson())
end end

View File

@ -2,11 +2,14 @@ local uci = require("uci").cursor()
local M = {} local M = {}
function string:split(sep) function string:split(div)
local sep, fields = sep or ":", {} local div, pos, arr = div or ":", 0, {}
local pattern = string.format("([^%s]+)", sep) for st,sp in function() return self:find(div, pos, true) end do
self:gsub(pattern, function(c) fields[#fields+1] = c end) table.insert(arr, self:sub(pos, st - 1))
return fields pos = sp + 1
end
table.insert(arr, self:sub(pos))
return arr
end end
function M.toboolean(s) function M.toboolean(s)