From ea8100ab60c9465b7485032f34f0f052b57679dc Mon Sep 17 00:00:00 2001 From: Wouter R Date: Mon, 8 Jul 2013 13:34:27 +0200 Subject: [PATCH] Logging now prints to stderr, which is redirected to a log file; cgi handler always responds with json; test API implemented; several bug fixes. --- src/logger.lua | 11 +- src/main.lua | 238 ++++------ src/main.old.lua | 173 +++++++ src/rest/api/api_network.lua | 95 ++++ src/rest/api/api_test.lua | 34 ++ src/rest/request.lua | 116 ++++- src/rest/response.lua | 44 ++ src/script/d3dapi | 13 +- src/util/JSON.lua | 861 +++++++++++++++++++++++++++++++++++ 9 files changed, 1420 insertions(+), 165 deletions(-) create mode 100644 src/main.old.lua create mode 100644 src/rest/api/api_network.lua create mode 100644 src/rest/api/api_test.lua create mode 100644 src/util/JSON.lua diff --git a/src/logger.lua b/src/logger.lua index d529ccf..2af1ed0 100644 --- a/src/logger.lua +++ b/src/logger.lua @@ -9,12 +9,17 @@ for i,v in ipairs(M.LEVEL) do M.LEVEL[v] = i end -function M:init(level, verbose, stream) +function M:init(level, verbose) logLevel = level or M.LEVEL.warn logVerbose = verbose or false logStream = stream or io.stdout end +--pass nil as stream to reset to stdout +function M:setStream(stream) + logStream = stream or io.stdout +end + local function log(level, msg, verbose) if level >= logLevel then local now = os.date("%m-%d %H:%M:%S") @@ -24,8 +29,8 @@ local function log(level, msg, verbose) local name = i.name or "(nil)" local vVal = "nil" local m = (type(msg) == "string") and msg or M:dump(msg) - if v then logStream:write(now .. " (" .. M.LEVEL[level] .. ") \t" .. m .. " [" .. name .. "@" .. i.short_src .. ":" .. i.linedefined .. "]\n") - else logStream:write(now .. " (" .. M.LEVEL[level] .. ") \t" .. m .. "\n") end + if v then logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. " [" .. name .. "@" .. i.short_src .. ":" .. i.linedefined .. "]\n") + else logStream:write(now .. " (" .. M.LEVEL[level] .. ") " .. m .. "\n") end end end diff --git a/src/main.lua b/src/main.lua index 758901c..083cdc7 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,173 +1,113 @@ ---[[ - Response format: - ["OK" | "WARN" | "ERR"]<,{message}> - {comma-separated line 1} - ... - {comma-separated line n} - - - general info on wireless config: http://wiki.openwrt.org/doc/uci/wireless - - uci docs: http://wiki.openwrt.org/doc/techref/uci - - parse/generate urls: https://github.com/keplerproject/cgilua/blob/master/src/cgilua/urlcode.lua - - utility functions: http://luci.subsignal.org/trac/browser/luci/trunk/libs/sys/luasrc/sys.lua - - iwinfo tool source: http://luci.subsignal.org/trac/browser/luci/trunk/contrib/package/iwinfo/src/iwinfo.lua?rev=7919 - - captive portal -> redirect all web traffic to one page for auth (or network selection) - http://wiki.openwrt.org/doc/howto/wireless.hotspot - ]] ---print ("HTTP/1.0 200 OK") -io.write ("Content-type: text/plain\r\n\r\n") - -local u = require("util") local l = require("logger") -local wifi = require("network.wlanconfig") -local reconf = require("network.netconfig") -local urlcode = require("util.urlcode") -local uci = require("uci").cursor() -local iwinfo = require("iwinfo") +local RequestClass = require("rest.request") +local ResponseClass = require("rest.response") -local argOperation, argDevice, argSsid, argPhrase, argRecreate -local errortext = nil -function init() - l:init(l.LEVEL.debug, true, io.stderr) - local qs = os.getenv("QUERY_STRING") - local urlargs = {} - urlcode.parsequery(qs, urlargs) +local DEBUG_PCALLS = false - --supplement urlargs with arguments from the command-line - for _, v in ipairs(arg) do - local split = v:find("=") - if split ~= nil then - urlargs[v:sub(1, split - 1)] = v:sub(split + 1) - end - end - argOperation = urlargs["op"] - argDevice = urlargs["dev"] or DFL_DEVICE - argSsid = urlargs["ssid"] - argPhrase = urlargs["phrase"] - argRecreate = urlargs["recreate"] +local postData = nil +local resp = ResponseClass.new() - if urlargs["echo"] ~= nil then - print("[[echo: '"..qs.."']]"); - end - if argOperation == nil then - errortext = "Missing operation specifier" - return false - end - - return wifi.init() and reconf.init(wifi, true) +local function setupAutoWifiMode() + io.write("--TODO: join known network if present, fall back to access point otherwise\n") end +local function init() + l:init(l.LEVEL.debug) + l:setStream(io.stderr) + + if DEBUG_PCALLS then l:info("Wifibox CGI handler started (pcall debugging enabled)") + else l:info("Wifibox CGI handler started") + end + + if (os.getenv("REQUEST_METHOD") == "POST") then + local n = tonumber(os.getenv("CONTENT_LENGTH")) + postData = io.read(n) + end +end -function main() - if argOperation == "getavl" then - local sr = wifi.getScanInfo() - local si, se - - --TODO: - -- - extend reconf 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) - if sr and #sr > 0 then - u.printWithSuccess(#sr .. " network(s) found"); - for _, se in ipairs(sr) do - --print("[[ " .. u.dump(se) .. " ]]") --TEMP - if se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then - print(se.ssid .. "," .. se.bssid .. "," .. se.channel .. "," .. wifi.mapDeviceMode(se.mode) .. "," .. wifi.mapEncryptionType(se.encryption)) - end - 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 - u.exitWithError("No scan results or scanning not possible") + return nil, ("function '" .. func .. "' does not exist in API module '" .. mod .. "'") end + end - elseif argOperation == "getknown" then - u.printWithSuccess("") - for _, net in ipairs(wifi.getConfigs()) do - if net.mode == "sta" then - local bssid = net.bssid or "" - local channel = net.channel or "" - print(net.ssid .. "," .. bssid .. "," .. channel) - 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 - elseif argOperation == "getstate" then - local ds = wifi.getDeviceState() - local ssid = ds.ssid or "" - local bssid = ds.bssid or "" - local channel = ds.channel or "" - u.printWithSuccess(""); - print(ssid .. "," .. bssid .. "," .. channel .. "," .. ds.mode) + l:info("received request of type " .. rq:getRequestMethod() .. " with arguments: " .. l:dump(rq:getAll())) + if rq:getRequestMethod() ~= "CMDLINE" then + l:info("remote IP/port: " .. rq:getRemoteHost() .. "/" .. rq:getRemotePort()) + l:debug("user agent: " .. rq:getUserAgent()) + end - elseif argOperation == "assoc" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to associate with") end - - local cfg = nil - for _, net in ipairs(wifi.getConfigs()) do - if net.mode ~= "ap" and net.ssid == argSsid then - cfg = net - break - end - end - if cfg == nil or argRecreate ~= nil then - local scanResult = wifi.getScanInfo(argSsid) - if scanResult ~= nil then - wifi.createConfigFromScanInfo(scanResult, argPhrase) - else - --check for error - u.exitWithError("No wireless network with SSID '" .. argSsid .. "' is available") - end - end - wifi.activateConfig(argSsid) - reconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } - u.exitWithSuccess("Wlan associated with network " .. argSsid .. "!") - - elseif argOperation == "disassoc" then - wifi.activateConfig() - local rv = wifi.restart() - u.exitWithSuccess("Deactivated all wireless networks [$?=" .. rv .. "]") - - elseif argOperation == "openap" then - --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed - reconf.switchConfiguration{apnet="add_noreload"} - wifi.activateConfig(wifi.AP_SSID) - reconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } - u.exitWithSuccess("Switched to AP mode (SSID: '" .. wifi.AP_SSID .. "')") - - elseif argOperation == "rm" then - if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to remove") end - if wifi.removeConfig(argSsid) then - u.exitWithSuccess("Removed wireless network with SSID " .. argSsid) + if (not DEBUG_PCALLS and rq:getRequestMethod() == "CMDLINE") then + if rq:get("autowifi") ~= nil then + setupAutoWifiMode() else - u.exitWithWarning("No wireless network with SSID " .. argSsid) + l:info("Nothing to do...bye.\n") end - elseif argOperation == "test" then - --invert actions performed by openap operation - reconf.switchConfiguration{ apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm" } --- reconf.switchConfiguration{dnsredir="add"} - u.exitWithSuccess("nop") - - elseif argOperation == "auto" then - u.exitWithWarning("Not implemented"); - --scan nets - --take union of scan and known - --connect to first if not empty; setup ap otherwise - else - u.exitWithError("Unknown operation: '" .. argOperation .. "'") + io.write ("Content-type: text/plain\r\n\r\n") + + 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 + 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 + resp:setError("function unknown '" .. mod .. "/" .. func .. "'") + print(resp:serializeAsJson()) + l:error("could not resolve requested API function ('" .. sr .. "')") + end end - - os.exit(0) end - ---[[ START OF CODE ]]-- - -if init() == false then - u.exitWithError(errortext) -end - +init() main() diff --git a/src/main.old.lua b/src/main.old.lua new file mode 100644 index 0000000..758901c --- /dev/null +++ b/src/main.old.lua @@ -0,0 +1,173 @@ +--[[ + Response format: + ["OK" | "WARN" | "ERR"]<,{message}> + {comma-separated line 1} + ... + {comma-separated line n} + + - general info on wireless config: http://wiki.openwrt.org/doc/uci/wireless + - uci docs: http://wiki.openwrt.org/doc/techref/uci + - parse/generate urls: https://github.com/keplerproject/cgilua/blob/master/src/cgilua/urlcode.lua + - utility functions: http://luci.subsignal.org/trac/browser/luci/trunk/libs/sys/luasrc/sys.lua + - iwinfo tool source: http://luci.subsignal.org/trac/browser/luci/trunk/contrib/package/iwinfo/src/iwinfo.lua?rev=7919 + - captive portal -> redirect all web traffic to one page for auth (or network selection) + http://wiki.openwrt.org/doc/howto/wireless.hotspot + ]] +--print ("HTTP/1.0 200 OK") +io.write ("Content-type: text/plain\r\n\r\n") + +local u = require("util") +local l = require("logger") +local wifi = require("network.wlanconfig") +local reconf = require("network.netconfig") +local urlcode = require("util.urlcode") +local uci = require("uci").cursor() +local iwinfo = require("iwinfo") + +local argOperation, argDevice, argSsid, argPhrase, argRecreate +local errortext = nil + +function init() + l:init(l.LEVEL.debug, true, io.stderr) + local qs = os.getenv("QUERY_STRING") + local urlargs = {} + urlcode.parsequery(qs, urlargs) + + --supplement urlargs with arguments from the command-line + for _, v in ipairs(arg) do + local split = v:find("=") + if split ~= nil then + urlargs[v:sub(1, split - 1)] = v:sub(split + 1) + end + end + + argOperation = urlargs["op"] + argDevice = urlargs["dev"] or DFL_DEVICE + argSsid = urlargs["ssid"] + argPhrase = urlargs["phrase"] + argRecreate = urlargs["recreate"] + + if urlargs["echo"] ~= nil then + print("[[echo: '"..qs.."']]"); + end + + if argOperation == nil then + errortext = "Missing operation specifier" + return false + end + + return wifi.init() and reconf.init(wifi, true) +end + + +function main() + if argOperation == "getavl" then + local sr = wifi.getScanInfo() + local si, se + + --TODO: + -- - extend reconf 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) + if sr and #sr > 0 then + u.printWithSuccess(#sr .. " network(s) found"); + for _, se in ipairs(sr) do + --print("[[ " .. u.dump(se) .. " ]]") --TEMP + if se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then + print(se.ssid .. "," .. se.bssid .. "," .. se.channel .. "," .. wifi.mapDeviceMode(se.mode) .. "," .. wifi.mapEncryptionType(se.encryption)) + end + end + else + u.exitWithError("No scan results or scanning not possible") + end + + elseif argOperation == "getknown" then + u.printWithSuccess("") + for _, net in ipairs(wifi.getConfigs()) do + if net.mode == "sta" then + local bssid = net.bssid or "" + local channel = net.channel or "" + print(net.ssid .. "," .. bssid .. "," .. channel) + end + end + + elseif argOperation == "getstate" then + local ds = wifi.getDeviceState() + local ssid = ds.ssid or "" + local bssid = ds.bssid or "" + local channel = ds.channel or "" + u.printWithSuccess(""); + print(ssid .. "," .. bssid .. "," .. channel .. "," .. ds.mode) + + elseif argOperation == "assoc" then + if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to associate with") end + + local cfg = nil + for _, net in ipairs(wifi.getConfigs()) do + if net.mode ~= "ap" and net.ssid == argSsid then + cfg = net + break + end + end + if cfg == nil or argRecreate ~= nil then + local scanResult = wifi.getScanInfo(argSsid) + if scanResult ~= nil then + wifi.createConfigFromScanInfo(scanResult, argPhrase) + else + --check for error + u.exitWithError("No wireless network with SSID '" .. argSsid .. "' is available") + end + end + wifi.activateConfig(argSsid) + reconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } + u.exitWithSuccess("Wlan associated with network " .. argSsid .. "!") + + elseif argOperation == "disassoc" then + wifi.activateConfig() + local rv = wifi.restart() + u.exitWithSuccess("Deactivated all wireless networks [$?=" .. rv .. "]") + + elseif argOperation == "openap" then + --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed + reconf.switchConfiguration{apnet="add_noreload"} + wifi.activateConfig(wifi.AP_SSID) + reconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } + u.exitWithSuccess("Switched to AP mode (SSID: '" .. wifi.AP_SSID .. "')") + + elseif argOperation == "rm" then + if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to remove") end + if wifi.removeConfig(argSsid) then + u.exitWithSuccess("Removed wireless network with SSID " .. argSsid) + else + u.exitWithWarning("No wireless network with SSID " .. argSsid) + end + + elseif argOperation == "test" then + --invert actions performed by openap operation + reconf.switchConfiguration{ apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm" } +-- reconf.switchConfiguration{dnsredir="add"} + u.exitWithSuccess("nop") + + elseif argOperation == "auto" then + u.exitWithWarning("Not implemented"); + --scan nets + --take union of scan and known + --connect to first if not empty; setup ap otherwise + + else + u.exitWithError("Unknown operation: '" .. argOperation .. "'") + end + + os.exit(0) +end + + + +--[[ START OF CODE ]]-- + +if init() == false then + u.exitWithError(errortext) +end + +main() diff --git a/src/rest/api/api_network.lua b/src/rest/api/api_network.lua new file mode 100644 index 0000000..a1a0927 --- /dev/null +++ b/src/rest/api/api_network.lua @@ -0,0 +1,95 @@ +local l = require("logger") + +local M = {} + +M.isApi = true + +function M._global(d) + return "not implemented..." +end + +--[[ + if argOperation == "getavl" then + local sr = wifi.getScanInfo() + local si, se + + --TODO: + -- - extend reconf 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) + if sr and #sr > 0 then + u.printWithSuccess(#sr .. " network(s) found"); + for _, se in ipairs(sr) do + --print("[[ " .. u.dump(se) .. " ]]") --TEMP + if se.mode ~= "ap" and se.ssid ~= wifi.AP_SSID then + print(se.ssid .. "," .. se.bssid .. "," .. se.channel .. "," .. wifi.mapDeviceMode(se.mode) .. "," .. wifi.mapEncryptionType(se.encryption)) + end + end + else + u.exitWithError("No scan results or scanning not possible") + end + + elseif argOperation == "getknown" then + u.printWithSuccess("") + for _, net in ipairs(wifi.getConfigs()) do + if net.mode == "sta" then + local bssid = net.bssid or "" + local channel = net.channel or "" + print(net.ssid .. "," .. bssid .. "," .. channel) + end + end + + elseif argOperation == "getstate" then + local ds = wifi.getDeviceState() + local ssid = ds.ssid or "" + local bssid = ds.bssid or "" + local channel = ds.channel or "" + u.printWithSuccess(""); + print(ssid .. "," .. bssid .. "," .. channel .. "," .. ds.mode) + + elseif argOperation == "assoc" then + if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to associate with") end + + local cfg = nil + for _, net in ipairs(wifi.getConfigs()) do + if net.mode ~= "ap" and net.ssid == argSsid then + cfg = net + break + end + end + if cfg == nil or argRecreate ~= nil then + local scanResult = wifi.getScanInfo(argSsid) + if scanResult ~= nil then + wifi.createConfigFromScanInfo(scanResult, argPhrase) + else + --check for error + u.exitWithError("No wireless network with SSID '" .. argSsid .. "' is available") + end + end + wifi.activateConfig(argSsid) + reconf.switchConfiguration{ wifiiface="add", apnet="rm", staticaddr="rm", dhcppool="rm", wwwredir="rm", dnsredir="rm", wwwcaptive="rm", wireless="reload" } + u.exitWithSuccess("Wlan associated with network " .. argSsid .. "!") + + elseif argOperation == "disassoc" then + wifi.activateConfig() + local rv = wifi.restart() + u.exitWithSuccess("Deactivated all wireless networks [$?=" .. rv .. "]") + + elseif argOperation == "openap" then + --add AP net, activate it, deactivate all others, reload network/wireless config, add all dhcp and captive settings and reload as needed + reconf.switchConfiguration{apnet="add_noreload"} + wifi.activateConfig(wifi.AP_SSID) + reconf.switchConfiguration{ wifiiface="add", network="reload", staticaddr="add", dhcppool="add", wwwredir="add", dnsredir="add", wwwcaptive="add" } + u.exitWithSuccess("Switched to AP mode (SSID: '" .. wifi.AP_SSID .. "')") + + elseif argOperation == "rm" then + if argSsid == nil or argSsid == "" then u.exitWithError("Please supply an SSID to remove") end + if wifi.removeConfig(argSsid) then + u.exitWithSuccess("Removed wireless network with SSID " .. argSsid) + else + u.exitWithWarning("No wireless network with SSID " .. argSsid) + end +]-- + +return M diff --git a/src/rest/api/api_test.lua b/src/rest/api/api_test.lua new file mode 100644 index 0000000..2f21e60 --- /dev/null +++ b/src/rest/api/api_test.lua @@ -0,0 +1,34 @@ +local l = require("logger") +local ResponseClass = require("rest.response") + +local M = {} + +M.isApi = true + +function M._global(d) + local r = ResponseClass.new() + local ba = d:getBlankArgument() or "" + r:setSuccess("REST test API - default function called with blank argument: '" .. ba .. "'") + return r +end + +function M.success(d) + local r = ResponseClass.new() + r:setSuccess("yay!") + return r +end + +function M.error(d) + local r = ResponseClass.new() + r:setError("this error has been generated on purpose") + return r +end + +function M.echo(d) + local r = ResponseClass.new() + r:setSuccess("request echo") + r:addData("request_data", d) + return r +end + +return M diff --git a/src/rest/request.lua b/src/rest/request.lua index 0d78839..faa025e 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -1,21 +1,113 @@ -local M = {} - local urlcode = require("util.urlcode") -function M:new() - --parse os.getenv("QUERY_STRING") - --parse os.getenv("REPLY")? - --parse arg +local M = {} +M.__index = M - local qs = os.getenv("QUERY_STRING") - local urlargs = {} - urlcode.parsequery(qs, urlargs) +local function kvTableFromUrlEncodedString(encodedText) + local args = {} + if (encodedText ~= nil) then + urlcode.parsequery(encodedText, args) + end + return args +end - --supplement urlargs with arguments from the command-line - for _, v in ipairs(arg) do +local function kvTableFromArray(argArray) + local args = {} + + for _, v in ipairs(argArray) do local split = v:find("=") if split ~= nil then - urlargs[v:sub(1, split - 1)] = v:sub(split + 1) + args[v:sub(1, split - 1)] = v:sub(split + 1) + else + args[v] = true end end + + return args end + + +setmetatable(M, { + __call = function(cls, ...) + return cls.new(...) + end +}) + +function M.new(postData, debug) + local self = setmetatable({}, M) + + --NOTE: is it correct to assume that absence of REQUEST_METHOD indicates command line invocation? + self.requestMethod = os.getenv("REQUEST_METHOD") + if self.requestMethod ~= nil then + self.remoteHost = os.getenv("REMOTE_HOST") + self.remotePort = os.getenv("REMOTE_PORT") + self.userAgent = os.getenv("HTTP_USER_AGENT") + else + self.requestMethod = "CMDLINE" + end + + self.cmdLineArgs = kvTableFromArray(arg) + self.getArgs = kvTableFromUrlEncodedString(os.getenv("QUERY_STRING")) + self.postArgs = kvTableFromUrlEncodedString(postData) + + --TEMP: until these can be extracted from the url path itself + self.apiModule = self.getArgs["m"] + self.apiFunction = self.getArgs["f"] + + if debug then + self.apiModule = self.cmdLineArgs["m"] or self.apiModule + self.apiFunction = self.cmdLineArgs["f"] or self.apiFunction + end + + return self +end + +function M:getRequestMethod() + return self.requestMethod +end + +function M:getApiModule() + return self.apiModule +end + +function M:getApiFunction() + return self.apiFunction +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:getRemotePort() return self.remotePort or 0 end +function M:getUserAgent() return self.userAgent or "" end + +function M:get(key) + if self.requestMethod == "GET" then + return self.getArgs[key] + elseif self.requestMethod == "POST" then + return self.postArgs[key] + elseif self.requestMethod == "CMDLINE" then + return self.cmdLineArgs[key] + else + return nil + end +end + +function M:getAll() + if self.requestMethod == "GET" then + return self.getArgs + elseif self.requestMethod == "POST" then + return self.postArgs + elseif self.requestMethod == "CMDLINE" then + return self.cmdLineArgs + else + return nil + end +end + +return M diff --git a/src/rest/response.lua b/src/rest/response.lua index e69de29..ed6001e 100644 --- a/src/rest/response.lua +++ b/src/rest/response.lua @@ -0,0 +1,44 @@ +local JSON = (loadfile "util/JSON.lua")() + +local M = {} +M.__index = M + +setmetatable(M, { + __call = function(cls, ...) + return cls.new(...) + end +}) + +function M.new() + local self = setmetatable({}, M) + + self.body = {status = nil, data = {}} + + return self +end + +function M:setStatus(s) + self.body.status = s +end + +function M:setSuccess(msg) + self.body.status = "success" + self.body.msg = msg +end + +function M:setError(msg) + self.body.status = "error" + self.body.msg = msg +end + +--NOTE: with this method, to add nested data, it is necessary to precreate the table and add it with its root key +--(e.g.: response:addData("data", {f1=3, f2="x"})) +function M:addData(k, v) + self.body.data[k] = v +end + +function M:serializeAsJson() + return JSON:encode(self.body) +end + +return M diff --git a/src/script/d3dapi b/src/script/d3dapi index a2a366a..870e627 100755 --- a/src/script/d3dapi +++ b/src/script/d3dapi @@ -3,6 +3,17 @@ LUA=lua SCRIPT_PATH=/usr/share/lua/wifibox +LOG_FILE=/tmp/wifibox.log cd $SCRIPT_PATH -$LUA ./main.lua $@ +$LUA ./main.lua $@ 2>> $LOG_FILE + +exit + +# Code below is for debugging incoming CGI data +read -n $CONTENT_LENGTH POSTDATA +echo -e "Content-type: text/plain\r\n\r\n" +set +echo "---" +echo $POSTDATA +echo "---" diff --git a/src/util/JSON.lua b/src/util/JSON.lua new file mode 100644 index 0000000..f38ea9d --- /dev/null +++ b/src/util/JSON.lua @@ -0,0 +1,861 @@ +-- -*- coding: utf-8 -*- +-- +-- Copyright 2010-2013 Jeffrey Friedl +-- http://regex.info/blog/ +-- +-- Latest copy: http://regex.info/blog/lua/json +-- +local VERSION = 20130120.6 -- version history at end of file +local OBJDEF = { VERSION = VERSION } + +-- +-- Simple JSON encoding and decoding in pure Lua. +-- http://www.json.org/ +-- +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +-- +-- +-- DECODING +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- If the JSON text is for an object or an array, e.g. +-- { "what": "books", "count": 3 } +-- or +-- [ "Larry", "Curly", "Moe" ] +-- +-- the result is a Lua table, e.g. +-- { what = "books", count = 3 } +-- or +-- { "Larry", "Curly", "Moe" } +-- +-- +-- The encode and decode routines accept an optional second argument, "etc", which is not used +-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any +-- type (including nil). +-- +-- With most errors during decoding, this code calls +-- +-- JSON:onDecodeError(message, text, location, etc) +-- +-- with a message about the error, and if known, the JSON text being parsed and the byte count +-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your +-- own function. +-- +-- The default onDecodeError() merely augments the message with data about the text and the +-- location if known (and if a second 'etc' argument had been provided to decode(), its value is +-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's +-- built-in assert(), and can also be overridden. +-- +-- For example, in an Adobe Lightroom plugin, you might use something like +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- LrErrors.throwUserError("Internal Error: invalid JSON data") +-- end +-- +-- or even just +-- +-- function JSON.assert(message) +-- LrErrors.throwUserError("Internal Error: " .. message) +-- end +-- +-- If JSON:decode() is passed a nil, this is called instead: +-- +-- JSON:onDecodeOfNilError(message, nil, nil, etc) +-- +-- and if JSON:decode() is passed HTML instead of JSON, this is called: +-- +-- JSON:onDecodeOfHTMLError(message, text, nil, etc) +-- +-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error +-- reporting, especially when you provide your own error-handling routines. Continuing with the +-- the Adobe Lightroom plugin example: +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- local note = "Internal Error: invalid JSON data" +-- if type(etc) = 'table' and etc.photo then +-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') +-- end +-- LrErrors.throwUserError(note) +-- end +-- +-- : +-- : +-- +-- for i, photo in ipairs(photosToProcess) do +-- : +-- : +-- local data = JSON:decode(someJsonText, { photo = photo }) +-- : +-- : +-- end +-- +-- +-- +-- + +-- DECODING AND STRICT TYPES +-- +-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally +-- possible to tell which a Lua table came from, or guarantee decode-encode round-trip +-- equivalency. +-- +-- However, if you enable strictTypes, e.g. +-- +-- JSON = (loadfile "JSON.lua")() --load the routines +-- JSON.strictTypes = true +-- +-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua +-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type. +-- +-- (This is not the default because other routines may not work well with tables that have a +-- metatable set, for example, Lightroom API calls.) +-- +-- +-- ENCODING +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability + +-- On error during encoding, this code calls: +-- +-- JSON:onEncodeError(message, etc) +-- +-- which you can override in your local JSON object. +-- +-- +-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT +-- +-- assert +-- onDecodeError +-- onDecodeOfNilError +-- onDecodeOfHTMLError +-- onEncodeError +-- +-- If you want to create a separate Lua JSON object with its own error handlers, +-- you can reload JSON.lua or use the :new() method. +-- +--------------------------------------------------------------------------- + + +local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-" +local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray +local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject + + +function OBJDEF:newArray(tbl) + return setmetatable(tbl or {}, isArray) +end + +function OBJDEF:newObject(tbl) + return setmetatable(tbl or {}, isObject) +end + +local function unicode_codepoint_as_utf8(codepoint) + -- + -- codepoint is a number + -- + if codepoint <= 127 then + return string.char(codepoint) + + elseif codepoint <= 2047 then + -- + -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 + -- + local highpart = math.floor(codepoint / 0x40) + local lowpart = codepoint - (0x40 * highpart) + return string.char(0xC0 + highpart, + 0x80 + lowpart) + + elseif codepoint <= 65535 then + -- + -- 1110yyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x1000) + local remainder = codepoint - 0x1000 * highpart + local midpart = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midpart + + highpart = 0xE0 + highpart + midpart = 0x80 + midpart + lowpart = 0x80 + lowpart + + -- + -- Check for an invalid character (thanks Andy R. at Adobe). + -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 + -- + if ( highpart == 0xE0 and midpart < 0xA0 ) or + ( highpart == 0xED and midpart > 0x9F ) or + ( highpart == 0xF0 and midpart < 0x90 ) or + ( highpart == 0xF4 and midpart > 0x8F ) + then + return "?" + else + return string.char(highpart, + midpart, + lowpart) + end + + else + -- + -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x40000) + local remainder = codepoint - 0x40000 * highpart + local midA = math.floor(remainder / 0x1000) + remainder = remainder - 0x1000 * midA + local midB = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midB + + return string.char(0xF0 + highpart, + 0x80 + midA, + 0x80 + midB, + 0x80 + lowpart) + end +end + +function OBJDEF:onDecodeError(message, text, location, etc) + if text then + if location then + message = string.format("%s at char %d of: %s", message, location, text) + else + message = string.format("%s: %s", message, text) + end + end + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError +OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError + +function OBJDEF:onEncodeError(message, etc) + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +local function grok_number(self, text, start, etc) + -- + -- Grab the integer part + -- + local integer_part = text:match('^-?[1-9]%d*', start) + or text:match("^-?0", start) + + if not integer_part then + self:onDecodeError("expected number", text, start, etc) + end + + local i = start + integer_part:len() + + -- + -- Grab an optional decimal part + -- + local decimal_part = text:match('^%.%d+', i) or "" + + i = i + decimal_part:len() + + -- + -- Grab an optional exponential part + -- + local exponent_part = text:match('^[eE][-+]?%d+', i) or "" + + i = i + exponent_part:len() + + local full_number_text = integer_part .. decimal_part .. exponent_part + local as_number = tonumber(full_number_text) + + if not as_number then + self:onDecodeError("bad number", text, start, etc) + end + + return as_number, i +end + + +local function grok_string(self, text, start, etc) + + if text:sub(start,start) ~= '"' then + self:onDecodeError("expected string's opening quote", text, start, etc) + end + + local i = start + 1 -- +1 to bypass the initial quote + local text_len = text:len() + local VALUE = "" + while i <= text_len do + local c = text:sub(i,i) + if c == '"' then + return VALUE, i + 1 + end + if c ~= '\\' then + VALUE = VALUE .. c + i = i + 1 + elseif text:match('^\\b', i) then + VALUE = VALUE .. "\b" + i = i + 2 + elseif text:match('^\\f', i) then + VALUE = VALUE .. "\f" + i = i + 2 + elseif text:match('^\\n', i) then + VALUE = VALUE .. "\n" + i = i + 2 + elseif text:match('^\\r', i) then + VALUE = VALUE .. "\r" + i = i + 2 + elseif text:match('^\\t', i) then + VALUE = VALUE .. "\t" + i = i + 2 + else + local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if hex then + i = i + 6 -- bypass what we just read + + -- We have a Unicode codepoint. It could be standalone, or if in the proper range and + -- followed by another in a specific range, it'll be a two-code surrogate pair. + local codepoint = tonumber(hex, 16) + if codepoint >= 0xD800 and codepoint <= 0xDBFF then + -- it's a hi surrogate... see whether we have a following low + local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if lo_surrogate then + i = i + 6 -- bypass the low surrogate we just read + codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) + else + -- not a proper low, so we'll just leave the first codepoint as is and spit it out. + end + end + VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + + else + + -- just pass through what's escaped + VALUE = VALUE .. text:match('^\\(.)', i) + i = i + 2 + end + end + end + + self:onDecodeError("unclosed string", text, start, etc) +end + +local function skip_whitespace(text, start) + + local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 + if match_end then + return match_end + 1 + else + return start + end +end + +local grok_one -- assigned later + +local function grok_object(self, text, start, etc) + if not text:sub(start,start) == '{' then + self:onDecodeError("expected '{'", text, start, etc) + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' + + local VALUE = self.strictTypes and self:newObject { } or { } + + if text:sub(i,i) == '}' then + return VALUE, i + 1 + end + local text_len = text:len() + while i <= text_len do + local key, new_i = grok_string(self, text, i, etc) + + i = skip_whitespace(text, new_i) + + if text:sub(i, i) ~= ':' then + self:onDecodeError("expected colon", text, i, etc) + end + + i = skip_whitespace(text, i + 1) + + local val, new_i = grok_one(self, text, i) + + VALUE[key] = val + + -- + -- Expect now either '}' to end things, or a ',' to allow us to continue. + -- + i = skip_whitespace(text, new_i) + + local c = text:sub(i,i) + + if c == '}' then + return VALUE, i + 1 + end + + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or '}'", text, i, etc) + end + + i = skip_whitespace(text, i + 1) + end + + self:onDecodeError("unclosed '{'", text, start, etc) +end + +local function grok_array(self, text, start, etc) + if not text:sub(start,start) == '[' then + self:onDecodeError("expected '['", text, start, etc) + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' + local VALUE = self.strictTypes and self:newArray { } or { } + if text:sub(i,i) == ']' then + return VALUE, i + 1 + end + + local text_len = text:len() + while i <= text_len do + local val, new_i = grok_one(self, text, i) + + table.insert(VALUE, val) + + i = skip_whitespace(text, new_i) + + -- + -- Expect now either ']' to end things, or a ',' to allow us to continue. + -- + local c = text:sub(i,i) + if c == ']' then + return VALUE, i + 1 + end + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or '['", text, i, etc) + end + i = skip_whitespace(text, i + 1) + end + self:onDecodeError("unclosed '['", text, start, etc) +end + + +grok_one = function(self, text, start, etc) + -- Skip any whitespace + start = skip_whitespace(text, start) + + if start > text:len() then + self:onDecodeError("unexpected end of string", text, nil, etc) + end + + if text:find('^"', start) then + return grok_string(self, text, start, etc) + + elseif text:find('^[-0123456789 ]', start) then + return grok_number(self, text, start, etc) + + elseif text:find('^%{', start) then + return grok_object(self, text, start, etc) + + elseif text:find('^%[', start) then + return grok_array(self, text, start, etc) + + elseif text:find('^true', start) then + return true, start + 4 + + elseif text:find('^false', start) then + return false, start + 5 + + elseif text:find('^null', start) then + return nil, start + 4 + + else + self:onDecodeError("can't parse JSON", text, start, etc) + end +end + +function OBJDEF:decode(text, etc) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) + end + + if text == nil then + self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) + elseif type(text) ~= 'string' then + self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) + end + + if text:match('^%s*$') then + return nil + end + + if text:match('^%s*<') then + -- Can't be JSON... we'll assume it's HTML + self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) + end + + -- + -- Ensure that it's not UTF-32 or UTF-16. + -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), + -- but this package can't handle them. + -- + if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then + self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) + end + + local success, value = pcall(grok_one, self, text, 1, etc) + if success then + return value + else + -- should never get here... JSON parse errors should have been caught earlier + assert(false, value) + return nil + end +end + +local function backslash_replacement_function(c) + if c == "\n" then + return "\\n" + elseif c == "\r" then + return "\\r" + elseif c == "\t" then + return "\\t" + elseif c == "\b" then + return "\\b" + elseif c == "\f" then + return "\\f" + elseif c == '"' then + return '\\"' + elseif c == '\\' then + return '\\\\' + else + return string.format("\\u%04x", c:byte()) + end +end + +local chars_to_be_escaped_in_JSON_string + = '[' + .. '"' -- class sub-pattern to match a double quote + .. '%\\' -- class sub-pattern to match a backslash + .. '%z' -- class sub-pattern to match a null + .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters + .. ']' + +local function json_string_literal(value) + local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) + return '"' .. newval .. '"' +end + +local function object_or_array(self, T, etc) + -- + -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON + -- object. If there are only numbers, it's a JSON array. + -- + -- If we'll be converting to a JSON object, we'll want to sort the keys so that the + -- end result is deterministic. + -- + local string_keys = { } + local seen_number_key = false + local maximum_number_key + + for key in pairs(T) do + if type(key) == 'number' then + seen_number_key = true + if not maximum_number_key or maximum_number_key < key then + maximum_number_key = key + end + elseif type(key) == 'string' then + table.insert(string_keys, key) + else + self:onEncodeError("can't encode table with a key of type " .. type(key), etc) + end + end + + if seen_number_key and #string_keys > 0 then + -- + -- Mixed key types... don't know what to do, so bail + -- + self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) + + elseif #string_keys == 0 then + -- + -- An array + -- + if seen_number_key then + return nil, maximum_number_key -- an array + else + -- + -- An empty table... + -- + if tostring(T) == "JSON array" then + return nil + elseif tostring(T) == "JSON object" then + return { } + else + -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects + return nil + end + end + else + -- + -- An object, so return a list of keys + -- + table.sort(string_keys) + return string_keys + end +end + +-- +-- Encode +-- +local encode_value -- must predeclare because it calls itself +function encode_value(self, value, parents, etc) + + + if value == nil then + return 'null' + end + + if type(value) == 'string' then + return json_string_literal(value) + elseif type(value) == 'number' then + if value ~= value then + -- + -- NaN (Not a Number). + -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. + -- + return "null" + elseif value >= math.huge then + -- + -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should + -- really be a package option. Note: at least with some implementations, positive infinity + -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. + -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" + -- case first. + -- + return "1e+9999" + elseif value <= -math.huge then + -- + -- Negative infinity. + -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. + -- + return "-1e+9999" + else + return tostring(value) + end + elseif type(value) == 'boolean' then + return tostring(value) + + elseif type(value) ~= 'table' then + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + + else + -- + -- A table to be converted to either a JSON object or array. + -- + local T = value + + if parents[T] then + self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) + else + parents[T] = true + end + + local result_value + + local object_keys, maximum_number_key = object_or_array(self, T, etc) + if maximum_number_key then + -- + -- An array... + -- + local ITEMS = { } + for i = 1, maximum_number_key do + table.insert(ITEMS, encode_value(self, T[i], parents, etc)) + end + + result_value = "[" .. table.concat(ITEMS, ",") .. "]" + elseif object_keys then + -- + -- An object + -- + + -- + -- We'll always sort the keys, so that comparisons can be made on + -- the results, etc. The actual order is not particularly + -- important (e.g. it doesn't matter what character set we sort + -- as); it's only important that it be deterministic... the same + -- every time. + -- + local PARTS = { } + for _, key in ipairs(object_keys) do + local encoded_key = encode_value(self, tostring(key), parents, etc) + local encoded_val = encode_value(self, T[key], parents, etc) + table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) + end + result_value = "{" .. table.concat(PARTS, ",") .. "}" + else + -- + -- An empty array/object... we'll treat it as an array, though it should really be an option + -- + result_value = "[]" + end + + parents[T] = false + return result_value + end +end + +local encode_pretty_value -- must predeclare because it calls itself +function encode_pretty_value(self, value, parents, indent, etc) + + if type(value) == 'string' then + return json_string_literal(value) + + elseif type(value) == 'number' then + return tostring(value) + + elseif type(value) == 'boolean' then + return tostring(value) + + elseif type(value) == 'nil' then + return 'null' + + elseif type(value) ~= 'table' then + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + + else + -- + -- A table to be converted to either a JSON object or array. + -- + local T = value + + if parents[T] then + self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) + end + parents[T] = true + + local result_value + + local object_keys = object_or_array(self, T, etc) + if not object_keys then + -- + -- An array... + -- + local ITEMS = { } + for i = 1, #T do + table.insert(ITEMS, encode_pretty_value(self, T[i], parents, indent, etc)) + end + + result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" + + else + + -- + -- An object -- can keys be numbers? + -- + + local KEYS = { } + local max_key_length = 0 + for _, key in ipairs(object_keys) do + local encoded = encode_pretty_value(self, tostring(key), parents, "", etc) + max_key_length = math.max(max_key_length, #encoded) + table.insert(KEYS, encoded) + end + local key_indent = indent .. " " + local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4) + local FORMAT = "%s%" .. tostring(max_key_length) .. "s: %s" + + local COMBINED_PARTS = { } + for i, key in ipairs(object_keys) do + local encoded_val = encode_pretty_value(self, T[key], parents, subtable_indent, etc) + table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) + end + result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" + end + + parents[T] = false + return result_value + end +end + +function OBJDEF:encode(value, etc) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) + end + + local parents = {} + return encode_value(self, value, parents, etc) +end + +function OBJDEF:encode_pretty(value, etc) + local parents = {} + local subtable_indent = "" + return encode_pretty_value(self, value, parents, subtable_indent, etc) +end + +function OBJDEF.__tostring() + return "JSON encode/decode package" +end + +OBJDEF.__index = OBJDEF + +function OBJDEF:new(args) + local new = { } + + if args then + for key, val in pairs(args) do + new[key] = val + end + end + + return setmetatable(new, OBJDEF) +end + +return OBJDEF:new() + +-- +-- Version history: +-- +-- 20130120.6 Comment update: added a link to the specific page on my blog where this code can +-- be found, so that folks who come across the code outside of my blog can find updates +-- more easily. +-- +-- 20111207.5 Added support for the 'etc' arguments, for better error reporting. +-- +-- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. +-- +-- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: +-- +-- * When encoding lua for JSON, Sparse numeric arrays are now handled by +-- spitting out full arrays, such that +-- JSON:encode({"one", "two", [10] = "ten"}) +-- returns +-- ["one","two",null,null,null,null,null,null,null,"ten"] +-- +-- In 20100810.2 and earlier, only up to the first non-null value would have been retained. +-- +-- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". +-- Version 20100810.2 and earlier created invalid JSON in both cases. +-- +-- * Unicode surrogate pairs are now detected when decoding JSON. +-- +-- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding +-- +-- 20100731.1 initial public release +--