Example: oBIX gateway with LogicMachine

Task

The oBIX module below is created for LogicMachine integration with SmartStruxure Lite with Digest Authentication user library.

oBIX to MPM with digest – use as event or resident based script

Source code    
  1. --======================================================================================================
  2. --******************************** MPM OBIX CONNECTION WITH DIGEST SUPPORT ***************************--
  3. --************************ Version 1.0 Created by Erwin van der Zwart 08-01-2016 *********************--
  4. --======================================================================================================
  5.  
  6. require('user.mpm_functions') -- See this library for mpm number(s) with valid credentials and IP settings
  7.  
  8. -- Select MPM (mpm number, autoresolve node id)
  9. mpm_settings = get_mpm_settings(1, true)
  10.  
  11. -- get data from mpm (mpm settings, object, item)
  12. result = get_data_from_mpm(mpm_settings, "AV61", "Present_Value")
  13. log(result)
  14.  
  15. -- post data to mpm (mpm number, object, item, value)
  16. post_data_to_mpm(mpm_settings, "AV61", "Present_Value", 111)
  17.  
  18. -- get data from mpm (mpm settings, object, item)
  19. result = get_data_from_mpm(mpm_settings, "AV62", "Present_Value")
  20. log(result)
  21.  
  22. -- post data to mpm (mpm number, object, item, value)
  23. post_data_to_mpm(mpm_settings, "AV62", "Present_Value", 222)

user.mpm_functions for oBIX to MPM with digest – user libary 1

Source code    
  1. --======================================================================================================
  2. --******************************* MPM FUNCTION LIBRARY WITH DIGEST SUPPORT ***************************--
  3. --************************ Version 1.0 Created by Erwin van der Zwart 08-01-2016 *********************--
  4. --======================================================================================================
  5.  
  6. local mpm = require "user.mpm_digest"
  7.  
  8. function get_mpm_settings(mpm_number,autoresolve)
  9.  
  10. function resolve_node()
  11. local url = "http://" .. mpm_username .. ":" .. mpm_password .. "@" .. mpm_ip
  12. local b, c, h = mpm.request(url)
  13. -- resolve node id automaticly (if not found we will use above but that one can be wrong)
  14. if h then
  15. mpm_node_id = string.sub(h.server, 1, 7)
  16. end
  17. return mpm_node_id
  18. end
  19.  
  20. --======================================================================================================
  21. --**************************************** Put here your MPM settings ********************************--
  22. --======================================================================================================
  23.  
  24. if mpm_number == 1 then -- Select MPM 1
  25. mpm_ip = '192.168.10.205'
  26. mpm_username = 'admin'
  27. mpm_password = 'Schneider'
  28. mpm_node_id = 'N004EAB'
  29. mpm_instance = 100
  30. elseif mpm_number == 2 then -- Select MPM 2
  31. mpm_ip = '192.168.10.206'
  32. mpm_username = 'admin'
  33. mpm_password = 'Schneider'
  34. mpm_node_id = 'N005EAB'
  35. mpm_instance = 150
  36. elseif mpm_number == 3 then -- Select MPM 3
  37. mpm_ip = '192.168.10.207'
  38. mpm_username = 'admin'
  39. mpm_password = 'Schneider'
  40. mpm_node_id = 'N006EAB'
  41. mpm_instance = 200
  42. else -- Select MPM with default settings (node ID is resolved automaticly)
  43. mpm_ip = '10.50.80.3'
  44. mpm_username = 'admin'
  45. mpm_password = 'admin'
  46. mpm_node_id = 'N000000'
  47. autoresolve = true
  48. mpm_instance = 100
  49. end
  50.  
  51. --======================================================================================================
  52. --******************************************** End of MPM settings ***********************************--
  53. --======================================================================================================
  54.  
  55.  
  56. MPM_Settings = {ip = mpm_ip, username = mpm_username, password = mpm_password, node = mpm_node_id, instance = mpm_instance}
  57.  
  58. if autoresolve then
  59. MPM_Settings.node = resolve_node()
  60. end
  61.  
  62. return MPM_Settings
  63. end
  64.  
  65. -- function to get data from mpm
  66. function get_data_from_mpm(mpm_settings,object,item)
  67. local url = "http://" .. mpm_settings.username .. ":" .. mpm_settings.password .. "@" .. mpm_settings.ip .. "/obix/network/" .. mpm_settings.node .. "/DEV" .. mpm_settings.instance .. "/" .. object .. "/" .. item
  68. local b, c, h = mpm.request(url)
  69. local value = b:match([[val="(.-)"]])
  70. local value = tonumber(value)
  71. return value
  72. end
  73.  
  74. -- function to post data to mpm
  75. function post_data_to_mpm(mpm_settings,object,item,value)
  76. local url = "http://" .. mpm_settings.username .. ":" .. mpm_settings.password .. "@" .. mpm_settings.ip .. "/obix/network/" .. mpm_settings.node .. "/DEV" .. mpm_settings.instance .. "/" .. object .. "/" .. item .. "/"
  77. local reqBody = [[<intl val="]] .. value .. [["/>]]
  78. local headers = {["Content-Type"] = "text/xml", ["Content-Length"] = #reqBody}
  79. local respTable = {}
  80. local returnList = {client = {}, code = {}, headers = {}, status = {}}
  81. local source=ltn12.source.string(reqBody)
  82. local sink=ltn12.sink.table(respTable)
  83. local result = mpm.request{url=url, method="POST", source=source, sink=sink, headers=headers}
  84. return result
  85. end

user.mpm_digest for oBIX to MPM with digest – user libary 2

Source code    
  1. --======================================================================================================
  2. --******************************* MPM DIGEST LIBRARY FOR DIGEST MD5 SUPPORT **************************--
  3. --************************ Version 1.0 Created by Erwin van der Zwart 08-01-2016 *********************--
  4. --======================================================================================================
  5.  
  6. local md5sum = nil
  7.  
  8. local md5 = {}
  9.  
  10. local char, byte, format, rep, sub =
  11. string.char, string.byte, string.format, string.rep, string.sub
  12. local bit_or, bit_and, bit_not, bit_xor, bit_rshift, bit_lshift
  13.  
  14. local ok, bit = pcall(require, 'bit')
  15. if ok then
  16. bit_or, bit_and, bit_not, bit_xor, bit_rshift, bit_lshift = bit.bor, bit.band, bit.bnot, bit.bxor, bit.rshift, bit.lshift
  17. else
  18. ok, bit = pcall(require, 'bit32')
  19.  
  20. if ok then
  21.  
  22. bit_not = bit.bnot
  23.  
  24. local tobit = function(n)
  25. return n <= 0x7fffffff and n or -(bit_not(n) + 1)
  26. end
  27.  
  28. local normalize = function(f)
  29. return function(a,b) return tobit(f(tobit(a), tobit(b))) end
  30. end
  31.  
  32. bit_or, bit_and, bit_xor = normalize(bit.bor), normalize(bit.band), normalize(bit.bxor)
  33. bit_rshift, bit_lshift = normalize(bit.rshift), normalize(bit.lshift)
  34.  
  35. else
  36.  
  37. local function tbl2number(tbl)
  38. local result = 0
  39. local power = 1
  40. for i = 1, #tbl do
  41. result = result + tbl[i] * power
  42. power = power * 2
  43. end
  44. return result
  45. end
  46.  
  47. local function expand(t1, t2)
  48. local big, small = t1, t2
  49. if(#big < #small) then
  50. big, small = small, big
  51. end
  52. for i = #small + 1, #big do
  53. small[i] = 0
  54. end
  55. end
  56.  
  57. local to_bits
  58.  
  59. bit_not = function(n)
  60. local tbl = to_bits(n)
  61. local size = math.max(#tbl, 32)
  62. for i = 1, size do
  63. if(tbl[i] == 1) then
  64. tbl[i] = 0
  65. else
  66. tbl[i] = 1
  67. end
  68. end
  69. return tbl2number(tbl)
  70. end
  71.  
  72. to_bits = function (n)
  73. if(n < 0) then
  74. return to_bits(bit_not(math.abs(n)) + 1)
  75. end
  76. local tbl = {}
  77. local cnt = 1
  78. local last
  79. while n > 0 do
  80. last = n % 2
  81. tbl[cnt] = last
  82. n = (n-last)/2
  83. cnt = cnt + 1
  84. end
  85.  
  86. return tbl
  87. end
  88.  
  89. bit_or = function(m, n)
  90. local tbl_m = to_bits(m)
  91. local tbl_n = to_bits(n)
  92. expand(tbl_m, tbl_n)
  93.  
  94. local tbl = {}
  95. for i = 1, #tbl_m do
  96. if(tbl_m[i]== 0 and tbl_n[i] == 0) then
  97. tbl[i] = 0
  98. else
  99. tbl[i] = 1
  100. end
  101. end
  102.  
  103. return tbl2number(tbl)
  104. end
  105.  
  106. bit_and = function(m, n)
  107. local tbl_m = to_bits(m)
  108. local tbl_n = to_bits(n)
  109. expand(tbl_m, tbl_n)
  110.  
  111. local tbl = {}
  112. for i = 1, #tbl_m do
  113. if(tbl_m[i]== 0 or tbl_n[i] == 0) then
  114. tbl[i] = 0
  115. else
  116. tbl[i] = 1
  117. end
  118. end
  119.  
  120. return tbl2number(tbl)
  121. end
  122.  
  123. bit_xor = function(m, n)
  124. local tbl_m = to_bits(m)
  125. local tbl_n = to_bits(n)
  126. expand(tbl_m, tbl_n)
  127.  
  128. local tbl = {}
  129. for i = 1, #tbl_m do
  130. if(tbl_m[i] ~= tbl_n[i]) then
  131. tbl[i] = 1
  132. else
  133. tbl[i] = 0
  134. end
  135. end
  136.  
  137. return tbl2number(tbl)
  138. end
  139.  
  140. bit_rshift = function(n, bits)
  141. local high_bit = 0
  142. if(n < 0) then
  143. n = bit_not(math.abs(n)) + 1
  144. high_bit = 0x80000000
  145. end
  146.  
  147. local floor = math.floor
  148.  
  149. for i=1, bits do
  150. n = n/2
  151. n = bit_or(floor(n), high_bit)
  152. end
  153. return floor(n)
  154. end
  155.  
  156. bit_lshift = function(n, bits)
  157. if(n < 0) then
  158. n = bit_not(math.abs(n)) + 1
  159. end
  160.  
  161. for i=1, bits do
  162. n = n*2
  163. end
  164. return bit_and(n, 0xFFFFFFFF)
  165. end
  166. end
  167. end
  168.  
  169. local function lei2str(i)
  170. local f=function (s) return char( bit_and( bit_rshift(i, s), 255)) end
  171. return f(0)..f(8)..f(16)..f(24)
  172. end
  173.  
  174. local function str2bei(s)
  175. local v=0
  176. for i=1, #s do
  177. v = v * 256 + byte(s, i)
  178. end
  179. return v
  180. end
  181.  
  182. local function str2lei(s)
  183. local v=0
  184. for i = #s,1,-1 do
  185. v = v*256 + byte(s, i)
  186. end
  187. return v
  188. end
  189.  
  190. local function cut_le_str(s,...)
  191. local o, r = 1, {}
  192. local args = {...}
  193. for i=1, #args do
  194. table.insert(r, str2lei(sub(s, o, o + args[i] - 1)))
  195. o = o + args[i]
  196. end
  197. return r
  198. end
  199.  
  200. local swap = function (w) return str2bei(lei2str(w)) end
  201.  
  202. local function hex2binaryaux(hexval)
  203. return char(tonumber(hexval, 16))
  204. end
  205.  
  206. local function hex2binary(hex)
  207. local result, _ = hex:gsub('..', hex2binaryaux)
  208. return result
  209. end
  210.  
  211. local CONSTS = {
  212. 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
  213. 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
  214. 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
  215. 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
  216. 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
  217. 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
  218. 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
  219. 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
  220. 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
  221. 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
  222. 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
  223. 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
  224. 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
  225. 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
  226. 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
  227. 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
  228. 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
  229. }
  230.  
  231. local f=function (x,y,z) return bit_or(bit_and(x,y),bit_and(-x-1,z)) end
  232. local g=function (x,y,z) return bit_or(bit_and(x,z),bit_and(y,-z-1)) end
  233. local h=function (x,y,z) return bit_xor(x,bit_xor(y,z)) end
  234. local i=function (x,y,z) return bit_xor(y,bit_or(x,-z-1)) end
  235. local z=function (f,a,b,c,d,x,s,ac)
  236. a=bit_and(a+f(b,c,d)+x+ac,0xFFFFFFFF)
  237. return bit_or(bit_lshift(bit_and(a,bit_rshift(0xFFFFFFFF,s)),s),bit_rshift(a,32-s))+b
  238. end
  239.  
  240. local function transform(A,B,C,D,X)
  241. local a,b,c,d=A,B,C,D
  242. local t=CONSTS
  243.  
  244. a=z(f,a,b,c,d,X[ 0], 7,t[ 1])
  245. d=z(f,d,a,b,c,X[ 1],12,t[ 2])
  246. c=z(f,c,d,a,b,X[ 2],17,t[ 3])
  247. b=z(f,b,c,d,a,X[ 3],22,t[ 4])
  248. a=z(f,a,b,c,d,X[ 4], 7,t[ 5])
  249. d=z(f,d,a,b,c,X[ 5],12,t[ 6])
  250. c=z(f,c,d,a,b,X[ 6],17,t[ 7])
  251. b=z(f,b,c,d,a,X[ 7],22,t[ 8])
  252. a=z(f,a,b,c,d,X[ 8], 7,t[ 9])
  253. d=z(f,d,a,b,c,X[ 9],12,t[10])
  254. c=z(f,c,d,a,b,X[10],17,t[11])
  255. b=z(f,b,c,d,a,X[11],22,t[12])
  256. a=z(f,a,b,c,d,X[12], 7,t[13])
  257. d=z(f,d,a,b,c,X[13],12,t[14])
  258. c=z(f,c,d,a,b,X[14],17,t[15])
  259. b=z(f,b,c,d,a,X[15],22,t[16])
  260.  
  261. a=z(g,a,b,c,d,X[ 1], 5,t[17])
  262. d=z(g,d,a,b,c,X[ 6], 9,t[18])
  263. c=z(g,c,d,a,b,X[11],14,t[19])
  264. b=z(g,b,c,d,a,X[ 0],20,t[20])
  265. a=z(g,a,b,c,d,X[ 5], 5,t[21])
  266. d=z(g,d,a,b,c,X[10], 9,t[22])
  267. c=z(g,c,d,a,b,X[15],14,t[23])
  268. b=z(g,b,c,d,a,X[ 4],20,t[24])
  269. a=z(g,a,b,c,d,X[ 9], 5,t[25])
  270. d=z(g,d,a,b,c,X[14], 9,t[26])
  271. c=z(g,c,d,a,b,X[ 3],14,t[27])
  272. b=z(g,b,c,d,a,X[ 8],20,t[28])
  273. a=z(g,a,b,c,d,X[13], 5,t[29])
  274. d=z(g,d,a,b,c,X[ 2], 9,t[30])
  275. c=z(g,c,d,a,b,X[ 7],14,t[31])
  276. b=z(g,b,c,d,a,X[12],20,t[32])
  277.  
  278. a=z(h,a,b,c,d,X[ 5], 4,t[33])
  279. d=z(h,d,a,b,c,X[ 8],11,t[34])
  280. c=z(h,c,d,a,b,X[11],16,t[35])
  281. b=z(h,b,c,d,a,X[14],23,t[36])
  282. a=z(h,a,b,c,d,X[ 1], 4,t[37])
  283. d=z(h,d,a,b,c,X[ 4],11,t[38])
  284. c=z(h,c,d,a,b,X[ 7],16,t[39])
  285. b=z(h,b,c,d,a,X[10],23,t[40])
  286. a=z(h,a,b,c,d,X[13], 4,t[41])
  287. d=z(h,d,a,b,c,X[ 0],11,t[42])
  288. c=z(h,c,d,a,b,X[ 3],16,t[43])
  289. b=z(h,b,c,d,a,X[ 6],23,t[44])
  290. a=z(h,a,b,c,d,X[ 9], 4,t[45])
  291. d=z(h,d,a,b,c,X[12],11,t[46])
  292. c=z(h,c,d,a,b,X[15],16,t[47])
  293. b=z(h,b,c,d,a,X[ 2],23,t[48])
  294.  
  295. a=z(i,a,b,c,d,X[ 0], 6,t[49])
  296. d=z(i,d,a,b,c,X[ 7],10,t[50])
  297. c=z(i,c,d,a,b,X[14],15,t[51])
  298. b=z(i,b,c,d,a,X[ 5],21,t[52])
  299. a=z(i,a,b,c,d,X[12], 6,t[53])
  300. d=z(i,d,a,b,c,X[ 3],10,t[54])
  301. c=z(i,c,d,a,b,X[10],15,t[55])
  302. b=z(i,b,c,d,a,X[ 1],21,t[56])
  303. a=z(i,a,b,c,d,X[ 8], 6,t[57])
  304. d=z(i,d,a,b,c,X[15],10,t[58])
  305. c=z(i,c,d,a,b,X[ 6],15,t[59])
  306. b=z(i,b,c,d,a,X[13],21,t[60])
  307. a=z(i,a,b,c,d,X[ 4], 6,t[61])
  308. d=z(i,d,a,b,c,X[11],10,t[62])
  309. c=z(i,c,d,a,b,X[ 2],15,t[63])
  310. b=z(i,b,c,d,a,X[ 9],21,t[64])
  311.  
  312. return A+a,B+b,C+c,D+d
  313. end
  314.  
  315. function md5.sumhexa(s)
  316. local msgLen = #s
  317. local padLen = 56 - msgLen % 64
  318.  
  319. if msgLen % 64 > 56 then padLen = padLen + 64 end
  320.  
  321. if padLen == 0 then padLen = 64 end
  322.  
  323. s = s .. char(128) .. rep(char(0),padLen-1) .. lei2str(8*msgLen) .. lei2str(0)
  324.  
  325. assert(#s % 64 == 0)
  326.  
  327. local t = CONSTS
  328. local a,b,c,d = t[65],t[66],t[67],t[68]
  329.  
  330. for i=1,#s,64 do
  331. local X = cut_le_str(sub(s,i,i+63),4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4)
  332. assert(#X == 16)
  333. X[0] = table.remove(X,1)
  334. a,b,c,d = transform(a,b,c,d,X)
  335. end
  336.  
  337. return format("%08x%08x%08x%08x",swap(a),swap(b),swap(c),swap(d))
  338. end
  339.  
  340. function md5.sum(s)
  341. return hex2binary(md5.sumhexa(s))
  342. end
  343.  
  344. do -- select MD5 library
  345.  
  346. local ok, mod = pcall(require, "crypto")
  347. if ok then
  348. local digest = (mod.evp or mod).digest
  349. if digest then
  350. md5sum = function(str) return digest("md5", str) end
  351. end
  352. end
  353.  
  354. if not md5sum then
  355. local md5 = (type(mod) == "table") and mod or md5
  356. md5sum = md5.sumhexa or md5.digest
  357. end
  358.  
  359. if not md5sum then
  360. ok = pcall(require, "digest") -- last because using globals
  361. if ok and md5 then md5sum = md5.digest end
  362. end
  363.  
  364. end
  365.  
  366. local s_http = require "socket.http"
  367. local s_url = require "socket.url"
  368. local ltn12 = require "ltn12"
  369.  
  370. local hash = function(...)
  371. return md5sum(table.concat({...}, ":"))
  372. end
  373.  
  374. local parse_header = function(h)
  375. local r = {}
  376. for k,v in (h .. ','):gmatch("(%w+)=(.-),") do
  377. if v:sub(1, 1) == '"' then -- strip quotes
  378. r[k:lower()] = v:sub(2, -2)
  379. else r[k:lower()] = v end
  380. end
  381. return r
  382. end
  383.  
  384. local make_digest_header = function(t)
  385. local s = {}
  386. local x
  387. for i=1,#t do
  388. x = t[i]
  389. if x.unquote then
  390. s[i] = x[1] .. '=' .. x[2]
  391. else
  392. s[i] = x[1] .. '="' .. x[2] .. '"'
  393. end
  394. end
  395. return "Digest " .. table.concat(s, ', ')
  396. end
  397.  
  398. local hcopy = function(t)
  399. local r = {}
  400. for k,v in pairs(t) do r[k] = v end
  401. return r
  402. end
  403.  
  404. local _request = function(t)
  405. if not t.url then error("missing URL") end
  406. local url = s_url.parse(t.url)
  407. local user, password = url.user, url.password
  408. if not (user and password) then
  409. error("missing credentials in URL")
  410. end
  411. url.user, url.password, url.authority, url.userinfo = nil, nil, nil, nil
  412. t.url = s_url.build(url)
  413. local ghost_source
  414. if t.source then
  415. local ghost_chunks = {}
  416. local ghost_capture = function(x)
  417. if x then ghost_chunks[#ghost_chunks+1] = x end
  418. return x
  419. end
  420. local ghost_i = 0
  421. ghost_source = function()
  422. ghost_i = ghost_i+1
  423. return ghost_chunks[ghost_i]
  424. end
  425. t.source = ltn12.source.chain(t.source, ghost_capture)
  426. end
  427. local b, c, h = s_http.request(t)
  428. if (c == 401) and h["www-authenticate"] then
  429. local ht = parse_header(h["www-authenticate"])
  430. assert(ht.realm and ht.nonce and ht.opaque)
  431. if ht.qop ~= "auth" then
  432. return nil, string.format("unsupported qop (%s)", tostring(ht.qop))
  433. end
  434. if ht.algorithm and (ht.algorithm:lower() ~= "md5") then
  435. return nil, string.format("unsupported algo (%s)", tostring(ht.algorithm))
  436. end
  437. local nc, cnonce = "00000001", string.format("%08x", os.time())
  438. local uri = s_url.build{path = url.path, query = url.query}
  439. local method = t.method or "GET"
  440. local response = hash(
  441. hash(user, ht.realm, password),
  442. ht.nonce,
  443. nc,
  444. cnonce,
  445. "auth",
  446. hash(method, uri)
  447. )
  448. t.headers = t.headers or {}
  449. t.headers.authorization = make_digest_header{
  450. {"username", user},
  451. {"realm", ht.realm},
  452. {"nonce", ht.nonce},
  453. {"uri", uri},
  454. {"cnonce", cnonce},
  455. {"nc", nc, unquote=true},
  456. {"qop", "auth"},
  457. {"algorithm", "MD5"},
  458. {"response", response},
  459. {"opaque", ht.opaque},
  460. }
  461. if not t.headers.cookie and h["set-cookie"] then
  462. local cookie = (h["set-cookie"] .. ";"):match("(.-=.-)[;,]")
  463. if cookie then
  464. t.headers.cookie = "$Version: 0; " .. cookie .. ";"
  465. end
  466. end
  467. if t.source then t.source = ghost_source end
  468. b, c, h = s_http.request(t)
  469. return b, c, h
  470. else return b, c, h end
  471. end
  472.  
  473. local request = function(x)
  474. local _t = type(x)
  475. if _t == "table" then
  476. return _request(hcopy(x))
  477. elseif _t == "string" then
  478. local r = {}
  479. local _, c, h = _request{url = x, sink = ltn12.sink.table(r)}
  480. return table.concat(r), c, h
  481. else error(string.format("unexpected type %s", _t)) end
  482. end
  483.  
  484. return {
  485. request = request,
  486. }

 

Created by Erwin van der Zwart