optimize session set, move lua libs to global
This commit is contained in:
parent
6264407d02
commit
39d14d0025
@ -42,22 +42,23 @@ var mathLuaCode string
|
|||||||
|
|
||||||
// ModuleInfo holds information about an embeddable Lua module
|
// ModuleInfo holds information about an embeddable Lua module
|
||||||
type ModuleInfo struct {
|
type ModuleInfo struct {
|
||||||
Name string // Module name
|
Name string // Module name
|
||||||
Code string // Module source code
|
Code string // Module source code
|
||||||
Bytecode atomic.Pointer[[]byte] // Cached bytecode
|
Bytecode atomic.Pointer[[]byte] // Cached bytecode
|
||||||
Once sync.Once // For one-time compilation
|
Once sync.Once // For one-time compilation
|
||||||
|
DefinesGlobal bool // Whether module defines globals directly
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sandbox = ModuleInfo{Name: "sandbox", Code: sandboxLuaCode}
|
sandbox = ModuleInfo{Name: "sandbox", Code: sandboxLuaCode}
|
||||||
modules = []ModuleInfo{
|
modules = []ModuleInfo{
|
||||||
{Name: "json", Code: jsonLuaCode},
|
{Name: "json", Code: jsonLuaCode, DefinesGlobal: true},
|
||||||
{Name: "sqlite", Code: sqliteLuaCode},
|
{Name: "sqlite", Code: sqliteLuaCode},
|
||||||
{Name: "fs", Code: fsLuaCode},
|
{Name: "fs", Code: fsLuaCode, DefinesGlobal: true},
|
||||||
{Name: "util", Code: utilLuaCode},
|
{Name: "util", Code: utilLuaCode, DefinesGlobal: true},
|
||||||
{Name: "string", Code: stringLuaCode},
|
{Name: "string", Code: stringLuaCode},
|
||||||
{Name: "table", Code: tableLuaCode},
|
{Name: "table", Code: tableLuaCode},
|
||||||
{Name: "crypto", Code: cryptoLuaCode},
|
{Name: "crypto", Code: cryptoLuaCode, DefinesGlobal: true},
|
||||||
{Name: "time", Code: timeLuaCode},
|
{Name: "time", Code: timeLuaCode},
|
||||||
{Name: "math", Code: mathLuaCode},
|
{Name: "math", Code: mathLuaCode},
|
||||||
}
|
}
|
||||||
@ -104,11 +105,18 @@ func loadModule(state *luajit.State, m *ModuleInfo, verbose bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := state.RunBytecodeWithResults(1); err != nil {
|
if m.DefinesGlobal {
|
||||||
return err
|
// Module defines its own globals, just run it
|
||||||
|
if err := state.RunBytecode(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Module returns a table, capture and set as global
|
||||||
|
if err := state.RunBytecodeWithResults(1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.SetGlobal(m.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.SetGlobal(m.Name)
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to interpreting the source
|
// Fallback to interpreting the source
|
||||||
if verbose {
|
if verbose {
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
crypto.lua - Cryptographic functions powered by Go
|
crypto.lua - Cryptographic functions powered by Go
|
||||||
]]--
|
]]--
|
||||||
|
|
||||||
local crypto = {}
|
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- HASHING FUNCTIONS
|
-- HASHING FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
@ -11,9 +9,9 @@ local crypto = {}
|
|||||||
-- Generate hash digest using various algorithms
|
-- Generate hash digest using various algorithms
|
||||||
-- Algorithms: md5, sha1, sha256, sha512
|
-- Algorithms: md5, sha1, sha256, sha512
|
||||||
-- Formats: hex (default), binary
|
-- Formats: hex (default), binary
|
||||||
function crypto.hash(data, algorithm, format)
|
function hash(data, algorithm, format)
|
||||||
if type(data) ~= "string" then
|
if type(data) ~= "string" then
|
||||||
error("crypto.hash: data must be a string", 2)
|
error("hash: data must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
algorithm = algorithm or "sha256"
|
algorithm = algorithm or "sha256"
|
||||||
@ -22,21 +20,20 @@ function crypto.hash(data, algorithm, format)
|
|||||||
return __crypto_hash(data, algorithm, format)
|
return __crypto_hash(data, algorithm, format)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Convenience functions for common hash algorithms
|
function md5(data, format)
|
||||||
function crypto.md5(data, format)
|
return hash(data, "md5", format)
|
||||||
return crypto.hash(data, "md5", format)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.sha1(data, format)
|
function sha1(data, format)
|
||||||
return crypto.hash(data, "sha1", format)
|
return hash(data, "sha1", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.sha256(data, format)
|
function sha256(data, format)
|
||||||
return crypto.hash(data, "sha256", format)
|
return hash(data, "sha256", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.sha512(data, format)
|
function sha512(data, format)
|
||||||
return crypto.hash(data, "sha512", format)
|
return hash(data, "sha512", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
@ -46,13 +43,13 @@ end
|
|||||||
-- Generate HMAC using various algorithms
|
-- Generate HMAC using various algorithms
|
||||||
-- Algorithms: md5, sha1, sha256, sha512
|
-- Algorithms: md5, sha1, sha256, sha512
|
||||||
-- Formats: hex (default), binary
|
-- Formats: hex (default), binary
|
||||||
function crypto.hmac(data, key, algorithm, format)
|
function hmac(data, key, algorithm, format)
|
||||||
if type(data) ~= "string" then
|
if type(data) ~= "string" then
|
||||||
error("crypto.hmac: data must be a string", 2)
|
error("hmac: data must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
if type(key) ~= "string" then
|
if type(key) ~= "string" then
|
||||||
error("crypto.hmac: key must be a string", 2)
|
error("hmac: key must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
algorithm = algorithm or "sha256"
|
algorithm = algorithm or "sha256"
|
||||||
@ -61,21 +58,20 @@ function crypto.hmac(data, key, algorithm, format)
|
|||||||
return __crypto_hmac(data, key, algorithm, format)
|
return __crypto_hmac(data, key, algorithm, format)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Convenience functions for common HMAC algorithms
|
function hmac_md5(data, key, format)
|
||||||
function crypto.hmac_md5(data, key, format)
|
return hmac(data, key, "md5", format)
|
||||||
return crypto.hmac(data, key, "md5", format)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.hmac_sha1(data, key, format)
|
function hmac_sha1(data, key, format)
|
||||||
return crypto.hmac(data, key, "sha1", format)
|
return hmac(data, key, "sha1", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.hmac_sha256(data, key, format)
|
function hmac_sha256(data, key, format)
|
||||||
return crypto.hmac(data, key, "sha256", format)
|
return hmac(data, key, "sha256", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
function crypto.hmac_sha512(data, key, format)
|
function hmac_sha512(data, key, format)
|
||||||
return crypto.hmac(data, key, "sha512", format)
|
return hmac(data, key, "sha512", format)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
@ -84,9 +80,9 @@ end
|
|||||||
|
|
||||||
-- Generate random bytes
|
-- Generate random bytes
|
||||||
-- Formats: binary (default), hex
|
-- Formats: binary (default), hex
|
||||||
function crypto.random_bytes(length, secure, format)
|
function random_bytes(length, secure, format)
|
||||||
if type(length) ~= "number" or length <= 0 then
|
if type(length) ~= "number" or length <= 0 then
|
||||||
error("crypto.random_bytes: length must be positive", 2)
|
error("random_bytes: length must be positive", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
secure = secure ~= false -- Default to secure
|
secure = secure ~= false -- Default to secure
|
||||||
@ -96,13 +92,13 @@ function crypto.random_bytes(length, secure, format)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Generate random integer in range [min, max]
|
-- Generate random integer in range [min, max]
|
||||||
function crypto.random_int(min, max, secure)
|
function random_int(min, max, secure)
|
||||||
if type(min) ~= "number" or type(max) ~= "number" then
|
if type(min) ~= "number" or type(max) ~= "number" then
|
||||||
error("crypto.random_int: min and max must be numbers", 2)
|
error("random_int: min and max must be numbers", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
if max <= min then
|
if max <= min then
|
||||||
error("crypto.random_int: max must be greater than min", 2)
|
error("random_int: max must be greater than min", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
secure = secure ~= false -- Default to secure
|
secure = secure ~= false -- Default to secure
|
||||||
@ -111,9 +107,9 @@ function crypto.random_int(min, max, secure)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Generate random string of specified length
|
-- Generate random string of specified length
|
||||||
function crypto.random_string(length, charset, secure)
|
function random_string(length, charset, secure)
|
||||||
if type(length) ~= "number" or length <= 0 then
|
if type(length) ~= "number" or length <= 0 then
|
||||||
error("crypto.random_string: length must be positive", 2)
|
error("random_string: length must be positive", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
secure = secure ~= false -- Default to secure
|
secure = secure ~= false -- Default to secure
|
||||||
@ -122,14 +118,14 @@ function crypto.random_string(length, charset, secure)
|
|||||||
charset = charset or "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
charset = charset or "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
if type(charset) ~= "string" or #charset == 0 then
|
if type(charset) ~= "string" or #charset == 0 then
|
||||||
error("crypto.random_string: charset must be non-empty", 2)
|
error("random_string: charset must be non-empty", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = ""
|
local result = ""
|
||||||
local charset_length = #charset
|
local charset_length = #charset
|
||||||
|
|
||||||
for i = 1, length do
|
for i = 1, length do
|
||||||
local index = crypto.random_int(1, charset_length, secure)
|
local index = random_int(1, charset_length, secure)
|
||||||
result = result .. charset:sub(index, index)
|
result = result .. charset:sub(index, index)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -141,8 +137,6 @@ end
|
|||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- Generate random UUID (v4)
|
-- Generate random UUID (v4)
|
||||||
function crypto.uuid()
|
function uuid()
|
||||||
return __crypto_uuid()
|
return __crypto_uuid()
|
||||||
end
|
end
|
||||||
|
|
||||||
return crypto
|
|
@ -1,49 +1,47 @@
|
|||||||
local fs = {}
|
function fs_read(path)
|
||||||
|
|
||||||
fs.read = function(path)
|
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.read: path must be a string", 2)
|
error("fs_read: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_read_file(path)
|
return __fs_read_file(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.write = function(path, content)
|
function fs_write(path, content)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.write: path must be a string", 2)
|
error("fs_write: path must be a string", 2)
|
||||||
end
|
end
|
||||||
if type(content) ~= "string" then
|
if type(content) ~= "string" then
|
||||||
error("fs.write: content must be a string", 2)
|
error("fs_write: content must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_write_file(path, content)
|
return __fs_write_file(path, content)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.append = function(path, content)
|
function fs_append(path, content)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.append: path must be a string", 2)
|
error("fs_append: path must be a string", 2)
|
||||||
end
|
end
|
||||||
if type(content) ~= "string" then
|
if type(content) ~= "string" then
|
||||||
error("fs.append: content must be a string", 2)
|
error("fs_append: content must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_append_file(path, content)
|
return __fs_append_file(path, content)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.exists = function(path)
|
function fs_exists(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.exists: path must be a string", 2)
|
error("fs_exists: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_exists(path)
|
return __fs_exists(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.remove = function(path)
|
function fs_remove(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.remove: path must be a string", 2)
|
error("fs_remove: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_remove_file(path)
|
return __fs_remove_file(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.info = function(path)
|
function fs_info(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.info: path must be a string", 2)
|
error("fs_info: path must be a string", 2)
|
||||||
end
|
end
|
||||||
local info = __fs_get_info(path)
|
local info = __fs_get_info(path)
|
||||||
|
|
||||||
@ -56,58 +54,58 @@ fs.info = function(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Directory Operations
|
-- Directory Operations
|
||||||
fs.mkdir = function(path, mode)
|
function fs_mkdir(path, mode)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.mkdir: path must be a string", 2)
|
error("fs_mkdir: path must be a string", 2)
|
||||||
end
|
end
|
||||||
mode = mode or 0755
|
mode = mode or 0755
|
||||||
return __fs_make_dir(path, mode)
|
return __fs_make_dir(path, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.ls = function(path)
|
function fs_ls(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.ls: path must be a string", 2)
|
error("fs_ls: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_list_dir(path)
|
return __fs_list_dir(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.rmdir = function(path, recursive)
|
function fs_rmdir(path, recursive)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.rmdir: path must be a string", 2)
|
error("fs_rmdir: path must be a string", 2)
|
||||||
end
|
end
|
||||||
recursive = recursive or false
|
recursive = recursive or false
|
||||||
return __fs_remove_dir(path, recursive)
|
return __fs_remove_dir(path, recursive)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Path Operations
|
-- Path Operations
|
||||||
fs.join_paths = function(...)
|
function fs_join_paths(...)
|
||||||
return __fs_join_paths(...)
|
return __fs_join_paths(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.dir_name = function(path)
|
function fs_dir_name(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.dir_name: path must be a string", 2)
|
error("fs_dir_name: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_dir_name(path)
|
return __fs_dir_name(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.base_name = function(path)
|
function fs_base_name(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.base_name: path must be a string", 2)
|
error("fs_base_name: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_base_name(path)
|
return __fs_base_name(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.extension = function(path)
|
function fs_extension(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
error("fs.extension: path must be a string", 2)
|
error("fs_extension: path must be a string", 2)
|
||||||
end
|
end
|
||||||
return __fs_extension(path)
|
return __fs_extension(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Utility Functions
|
-- Utility Functions
|
||||||
fs.read_json = function(path)
|
function fs_read_json(path)
|
||||||
local content = fs.read_file(path)
|
local content = fs_read(path)
|
||||||
if not content then
|
if not content then
|
||||||
return nil, "Could not read file"
|
return nil, "Could not read file"
|
||||||
end
|
end
|
||||||
@ -120,9 +118,9 @@ fs.read_json = function(path)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.write_json = function(path, data, pretty)
|
function fs_write_json(path, data, pretty)
|
||||||
if type(data) ~= "table" then
|
if type(data) ~= "table" then
|
||||||
error("fs.write_json: data must be a table", 2)
|
error("fs_write_json: data must be a table", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local content
|
local content
|
||||||
@ -132,7 +130,5 @@ fs.write_json = function(path, data, pretty)
|
|||||||
content = json.encode(data)
|
content = json.encode(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
return fs.write_file(path, content)
|
return fs_write(path, content)
|
||||||
end
|
end
|
||||||
|
|
||||||
return fs
|
|
@ -1,18 +1,23 @@
|
|||||||
-- json.lua: High-performance JSON module for Moonshark
|
-- json.lua: High-performance JSON module for Moonshark
|
||||||
local json = {}
|
|
||||||
|
|
||||||
function json.go_encode(value)
|
-- Pre-computed escape sequences to avoid recreating table
|
||||||
return __json_marshal(value)
|
local escape_chars = {
|
||||||
|
['"'] = '\\"', ['\\'] = '\\\\',
|
||||||
|
['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
function json_go_encode(value)
|
||||||
|
return __json_marshal(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.go_decode(str)
|
function json_go_decode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
error("json.decode: expected string, got " .. type(str), 2)
|
error("json_decode: expected string, got " .. type(str), 2)
|
||||||
end
|
end
|
||||||
return __json_unmarshal(str)
|
return __json_unmarshal(str)
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.encode(data)
|
function json_encode(data)
|
||||||
local t = type(data)
|
local t = type(data)
|
||||||
|
|
||||||
if t == "nil" then return "null" end
|
if t == "nil" then return "null" end
|
||||||
@ -20,50 +25,36 @@ function json.encode(data)
|
|||||||
if t == "number" then return tostring(data) end
|
if t == "number" then return tostring(data) end
|
||||||
|
|
||||||
if t == "string" then
|
if t == "string" then
|
||||||
local escape_chars = {
|
|
||||||
['"'] = '\\"', ['\\'] = '\\\\',
|
|
||||||
['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t'
|
|
||||||
}
|
|
||||||
return '"' .. data:gsub('[\\"\n\r\t]', escape_chars) .. '"'
|
return '"' .. data:gsub('[\\"\n\r\t]', escape_chars) .. '"'
|
||||||
end
|
end
|
||||||
|
|
||||||
if t == "table" then
|
if t == "table" then
|
||||||
local isArray = true
|
local isArray = true
|
||||||
local count = 0
|
local count = 0
|
||||||
local max_index = 0
|
|
||||||
|
|
||||||
|
-- Check if it's an array in one pass
|
||||||
for k, _ in pairs(data) do
|
for k, _ in pairs(data) do
|
||||||
count = count + 1
|
count = count + 1
|
||||||
if type(k) == "number" and k > 0 and math.floor(k) == k then
|
if type(k) ~= "number" or k ~= count or k < 1 then
|
||||||
max_index = math.max(max_index, k)
|
isArray = false
|
||||||
else
|
break
|
||||||
isArray = false
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
if isArray then
|
if isArray then
|
||||||
for i, v in ipairs(data) do
|
local result = {}
|
||||||
result[i] = json.encode(v)
|
for i = 1, count do
|
||||||
|
result[i] = json_encode(data[i])
|
||||||
end
|
end
|
||||||
return "[" .. table.concat(result, ",") .. "]"
|
return "[" .. table.concat(result, ",") .. "]"
|
||||||
else
|
else
|
||||||
local size = 0
|
local result = {}
|
||||||
for k, v in pairs(data) do
|
|
||||||
if type(k) == "string" and type(v) ~= "function" and type(v) ~= "userdata" then
|
|
||||||
size = size + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
local index = 1
|
local index = 1
|
||||||
for k, v in pairs(data) do
|
for k, v in pairs(data) do
|
||||||
if type(k) == "string" and type(v) ~= "function" and type(v) ~= "userdata" then
|
if type(k) == "string" and type(v) ~= "function" and type(v) ~= "userdata" then
|
||||||
result[index] = json.encode(k) .. ":" .. json.encode(v)
|
result[index] = json_encode(k) .. ":" .. json_encode(v)
|
||||||
index = index + 1
|
index = index + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return "{" .. table.concat(result, ",") .. "}"
|
return "{" .. table.concat(result, ",") .. "}"
|
||||||
end
|
end
|
||||||
@ -72,7 +63,7 @@ function json.encode(data)
|
|||||||
return "null" -- Unsupported type
|
return "null" -- Unsupported type
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.decode(data)
|
function json_decode(data)
|
||||||
local pos = 1
|
local pos = 1
|
||||||
local len = #data
|
local len = #data
|
||||||
|
|
||||||
@ -100,14 +91,14 @@ function json.decode(data)
|
|||||||
|
|
||||||
-- Skip whitespace more efficiently
|
-- Skip whitespace more efficiently
|
||||||
local function skip()
|
local function skip()
|
||||||
local b
|
local b
|
||||||
while pos <= len do
|
while pos <= len do
|
||||||
b = data:byte(pos)
|
b = data:byte(pos)
|
||||||
if b > b_space or (b ~= b_space and b ~= b_tab and b ~= b_cr and b ~= b_lf) then
|
if b > b_space or (b ~= b_space and b ~= b_tab and b ~= b_cr and b ~= b_lf) then
|
||||||
break
|
break
|
||||||
|
end
|
||||||
|
pos = pos + 1
|
||||||
end
|
end
|
||||||
pos = pos + 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Forward declarations
|
-- Forward declarations
|
||||||
@ -115,250 +106,250 @@ function json.decode(data)
|
|||||||
|
|
||||||
-- Parse a string more efficiently
|
-- Parse a string more efficiently
|
||||||
parse_string = function()
|
parse_string = function()
|
||||||
pos = pos + 1 -- Skip opening quote
|
pos = pos + 1 -- Skip opening quote
|
||||||
|
|
||||||
if pos > len then
|
if pos > len then
|
||||||
error("Unterminated string")
|
error("Unterminated string")
|
||||||
end
|
|
||||||
|
|
||||||
-- Use a table to build the string
|
|
||||||
local result = {}
|
|
||||||
local result_pos = 1
|
|
||||||
local start = pos
|
|
||||||
local c, b
|
|
||||||
|
|
||||||
while pos <= len do
|
|
||||||
b = data:byte(pos)
|
|
||||||
|
|
||||||
if b == b_backslash then
|
|
||||||
-- Add the chunk before the escape character
|
|
||||||
if pos > start then
|
|
||||||
result[result_pos] = data:sub(start, pos - 1)
|
|
||||||
result_pos = result_pos + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len then
|
|
||||||
error("Unterminated string escape")
|
|
||||||
end
|
|
||||||
|
|
||||||
c = data:byte(pos)
|
|
||||||
if c == b_quote then
|
|
||||||
result[result_pos] = '"'
|
|
||||||
elseif c == b_backslash then
|
|
||||||
result[result_pos] = '\\'
|
|
||||||
elseif c == b_slash then
|
|
||||||
result[result_pos] = '/'
|
|
||||||
elseif c == string.byte('b') then
|
|
||||||
result[result_pos] = '\b'
|
|
||||||
elseif c == string.byte('f') then
|
|
||||||
result[result_pos] = '\f'
|
|
||||||
elseif c == string.byte('n') then
|
|
||||||
result[result_pos] = '\n'
|
|
||||||
elseif c == string.byte('r') then
|
|
||||||
result[result_pos] = '\r'
|
|
||||||
elseif c == string.byte('t') then
|
|
||||||
result[result_pos] = '\t'
|
|
||||||
else
|
|
||||||
result[result_pos] = data:sub(pos, pos)
|
|
||||||
end
|
|
||||||
|
|
||||||
result_pos = result_pos + 1
|
|
||||||
pos = pos + 1
|
|
||||||
start = pos
|
|
||||||
elseif b == b_quote then
|
|
||||||
-- Add the final chunk
|
|
||||||
if pos > start then
|
|
||||||
result[result_pos] = data:sub(start, pos - 1)
|
|
||||||
result_pos = result_pos + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
pos = pos + 1
|
|
||||||
return table.concat(result)
|
|
||||||
else
|
|
||||||
pos = pos + 1
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
error("Unterminated string")
|
-- Use a table to build the string
|
||||||
|
local result = {}
|
||||||
|
local result_pos = 1
|
||||||
|
local start = pos
|
||||||
|
local c, b
|
||||||
|
|
||||||
|
while pos <= len do
|
||||||
|
b = data:byte(pos)
|
||||||
|
|
||||||
|
if b == b_backslash then
|
||||||
|
-- Add the chunk before the escape character
|
||||||
|
if pos > start then
|
||||||
|
result[result_pos] = data:sub(start, pos - 1)
|
||||||
|
result_pos = result_pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len then
|
||||||
|
error("Unterminated string escape")
|
||||||
|
end
|
||||||
|
|
||||||
|
c = data:byte(pos)
|
||||||
|
if c == b_quote then
|
||||||
|
result[result_pos] = '"'
|
||||||
|
elseif c == b_backslash then
|
||||||
|
result[result_pos] = '\\'
|
||||||
|
elseif c == b_slash then
|
||||||
|
result[result_pos] = '/'
|
||||||
|
elseif c == string.byte('b') then
|
||||||
|
result[result_pos] = '\b'
|
||||||
|
elseif c == string.byte('f') then
|
||||||
|
result[result_pos] = '\f'
|
||||||
|
elseif c == string.byte('n') then
|
||||||
|
result[result_pos] = '\n'
|
||||||
|
elseif c == string.byte('r') then
|
||||||
|
result[result_pos] = '\r'
|
||||||
|
elseif c == string.byte('t') then
|
||||||
|
result[result_pos] = '\t'
|
||||||
|
else
|
||||||
|
result[result_pos] = data:sub(pos, pos)
|
||||||
|
end
|
||||||
|
|
||||||
|
result_pos = result_pos + 1
|
||||||
|
pos = pos + 1
|
||||||
|
start = pos
|
||||||
|
elseif b == b_quote then
|
||||||
|
-- Add the final chunk
|
||||||
|
if pos > start then
|
||||||
|
result[result_pos] = data:sub(start, pos - 1)
|
||||||
|
result_pos = result_pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
pos = pos + 1
|
||||||
|
return table.concat(result)
|
||||||
|
else
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
error("Unterminated string")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse a number more efficiently
|
-- Parse a number more efficiently
|
||||||
parse_number = function()
|
parse_number = function()
|
||||||
local start = pos
|
local start = pos
|
||||||
local b = data:byte(pos)
|
local b = data:byte(pos)
|
||||||
|
|
||||||
-- Skip any sign
|
-- Skip any sign
|
||||||
if b == b_minus then
|
if b == b_minus then
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
if pos > len then
|
if pos > len then
|
||||||
error("Malformed number")
|
error("Malformed number")
|
||||||
end
|
end
|
||||||
b = data:byte(pos)
|
b = data:byte(pos)
|
||||||
end
|
|
||||||
|
|
||||||
-- Integer part
|
|
||||||
if b < b_0 or b > b_9 then
|
|
||||||
error("Malformed number")
|
|
||||||
end
|
|
||||||
|
|
||||||
repeat
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len then break end
|
|
||||||
b = data:byte(pos)
|
|
||||||
until b < b_0 or b > b_9
|
|
||||||
|
|
||||||
-- Fractional part
|
|
||||||
if pos <= len and b == b_dot then
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len or data:byte(pos) < b_0 or data:byte(pos) > b_9 then
|
|
||||||
error("Malformed number")
|
|
||||||
end
|
|
||||||
|
|
||||||
repeat
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len then break end
|
|
||||||
b = data:byte(pos)
|
|
||||||
until b < b_0 or b > b_9
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Exponent
|
|
||||||
if pos <= len and (b == b_e or b == b_E) then
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len then
|
|
||||||
error("Malformed number")
|
|
||||||
end
|
|
||||||
|
|
||||||
b = data:byte(pos)
|
|
||||||
if b == b_plus or b == b_minus then
|
|
||||||
pos = pos + 1
|
|
||||||
if pos > len then
|
|
||||||
error("Malformed number")
|
|
||||||
end
|
|
||||||
b = data:byte(pos)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Integer part
|
||||||
if b < b_0 or b > b_9 then
|
if b < b_0 or b > b_9 then
|
||||||
error("Malformed number")
|
error("Malformed number")
|
||||||
end
|
end
|
||||||
|
|
||||||
repeat
|
repeat
|
||||||
pos = pos + 1
|
pos = pos + 1
|
||||||
if pos > len then break end
|
if pos > len then break end
|
||||||
b = data:byte(pos)
|
b = data:byte(pos)
|
||||||
until b < b_0 or b > b_9
|
until b < b_0 or b > b_9
|
||||||
end
|
|
||||||
|
|
||||||
return tonumber(data:sub(start, pos - 1))
|
-- Fractional part
|
||||||
|
if pos <= len and b == b_dot then
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len or data:byte(pos) < b_0 or data:byte(pos) > b_9 then
|
||||||
|
error("Malformed number")
|
||||||
|
end
|
||||||
|
|
||||||
|
repeat
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len then break end
|
||||||
|
b = data:byte(pos)
|
||||||
|
until b < b_0 or b > b_9
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Exponent
|
||||||
|
if pos <= len and (b == b_e or b == b_E) then
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len then
|
||||||
|
error("Malformed number")
|
||||||
|
end
|
||||||
|
|
||||||
|
b = data:byte(pos)
|
||||||
|
if b == b_plus or b == b_minus then
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len then
|
||||||
|
error("Malformed number")
|
||||||
|
end
|
||||||
|
b = data:byte(pos)
|
||||||
|
end
|
||||||
|
|
||||||
|
if b < b_0 or b > b_9 then
|
||||||
|
error("Malformed number")
|
||||||
|
end
|
||||||
|
|
||||||
|
repeat
|
||||||
|
pos = pos + 1
|
||||||
|
if pos > len then break end
|
||||||
|
b = data:byte(pos)
|
||||||
|
until b < b_0 or b > b_9
|
||||||
|
end
|
||||||
|
|
||||||
|
return tonumber(data:sub(start, pos - 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse an object more efficiently
|
-- Parse an object more efficiently
|
||||||
parse_object = function()
|
parse_object = function()
|
||||||
pos = pos + 1 -- Skip opening brace
|
pos = pos + 1 -- Skip opening brace
|
||||||
local obj = {}
|
local obj = {}
|
||||||
|
|
||||||
skip()
|
|
||||||
if pos <= len and data:byte(pos) == b_rcurly then
|
|
||||||
pos = pos + 1
|
|
||||||
return obj
|
|
||||||
end
|
|
||||||
|
|
||||||
while pos <= len do
|
|
||||||
skip()
|
skip()
|
||||||
|
if pos <= len and data:byte(pos) == b_rcurly then
|
||||||
if data:byte(pos) ~= b_quote then
|
pos = pos + 1
|
||||||
error("Expected string key")
|
return obj
|
||||||
end
|
end
|
||||||
|
|
||||||
local key = parse_string()
|
while pos <= len do
|
||||||
skip()
|
skip()
|
||||||
|
|
||||||
if data:byte(pos) ~= b_colon then
|
if data:byte(pos) ~= b_quote then
|
||||||
error("Expected colon")
|
error("Expected string key")
|
||||||
end
|
end
|
||||||
pos = pos + 1
|
|
||||||
|
|
||||||
obj[key] = parse_value()
|
local key = parse_string()
|
||||||
skip()
|
skip()
|
||||||
|
|
||||||
local b = data:byte(pos)
|
if data:byte(pos) ~= b_colon then
|
||||||
if b == b_rcurly then
|
error("Expected colon")
|
||||||
pos = pos + 1
|
end
|
||||||
return obj
|
pos = pos + 1
|
||||||
|
|
||||||
|
obj[key] = parse_value()
|
||||||
|
skip()
|
||||||
|
|
||||||
|
local b = data:byte(pos)
|
||||||
|
if b == b_rcurly then
|
||||||
|
pos = pos + 1
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
if b ~= b_comma then
|
||||||
|
error("Expected comma or closing brace")
|
||||||
|
end
|
||||||
|
pos = pos + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
if b ~= b_comma then
|
error("Unterminated object")
|
||||||
error("Expected comma or closing brace")
|
|
||||||
end
|
|
||||||
pos = pos + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
error("Unterminated object")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse an array more efficiently
|
-- Parse an array more efficiently
|
||||||
parse_array = function()
|
parse_array = function()
|
||||||
pos = pos + 1 -- Skip opening bracket
|
pos = pos + 1 -- Skip opening bracket
|
||||||
local arr = {}
|
local arr = {}
|
||||||
local index = 1
|
local index = 1
|
||||||
|
|
||||||
skip()
|
|
||||||
if pos <= len and data:byte(pos) == b_rbracket then
|
|
||||||
pos = pos + 1
|
|
||||||
return arr
|
|
||||||
end
|
|
||||||
|
|
||||||
while pos <= len do
|
|
||||||
arr[index] = parse_value()
|
|
||||||
index = index + 1
|
|
||||||
|
|
||||||
skip()
|
skip()
|
||||||
|
if pos <= len and data:byte(pos) == b_rbracket then
|
||||||
local b = data:byte(pos)
|
pos = pos + 1
|
||||||
if b == b_rbracket then
|
return arr
|
||||||
pos = pos + 1
|
|
||||||
return arr
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if b ~= b_comma then
|
while pos <= len do
|
||||||
error("Expected comma or closing bracket")
|
arr[index] = parse_value()
|
||||||
end
|
index = index + 1
|
||||||
pos = pos + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
error("Unterminated array")
|
skip()
|
||||||
|
|
||||||
|
local b = data:byte(pos)
|
||||||
|
if b == b_rbracket then
|
||||||
|
pos = pos + 1
|
||||||
|
return arr
|
||||||
|
end
|
||||||
|
|
||||||
|
if b ~= b_comma then
|
||||||
|
error("Expected comma or closing bracket")
|
||||||
|
end
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
error("Unterminated array")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse a value more efficiently
|
-- Parse a value more efficiently
|
||||||
parse_value = function()
|
parse_value = function()
|
||||||
skip()
|
skip()
|
||||||
|
|
||||||
if pos > len then
|
if pos > len then
|
||||||
error("Unexpected end of input")
|
error("Unexpected end of input")
|
||||||
end
|
end
|
||||||
|
|
||||||
local b = data:byte(pos)
|
local b = data:byte(pos)
|
||||||
|
|
||||||
if b == b_quote then
|
if b == b_quote then
|
||||||
return parse_string()
|
return parse_string()
|
||||||
elseif b == b_lcurly then
|
elseif b == b_lcurly then
|
||||||
return parse_object()
|
return parse_object()
|
||||||
elseif b == b_lbracket then
|
elseif b == b_lbracket then
|
||||||
return parse_array()
|
return parse_array()
|
||||||
elseif b == string.byte('n') and pos + 3 <= len and data:sub(pos, pos + 3) == "null" then
|
elseif b == string.byte('n') and pos + 3 <= len and data:sub(pos, pos + 3) == "null" then
|
||||||
pos = pos + 4
|
pos = pos + 4
|
||||||
return nil
|
return nil
|
||||||
elseif b == string.byte('t') and pos + 3 <= len and data:sub(pos, pos + 3) == "true" then
|
elseif b == string.byte('t') and pos + 3 <= len and data:sub(pos, pos + 3) == "true" then
|
||||||
pos = pos + 4
|
pos = pos + 4
|
||||||
return true
|
return true
|
||||||
elseif b == string.byte('f') and pos + 4 <= len and data:sub(pos, pos + 4) == "false" then
|
elseif b == string.byte('f') and pos + 4 <= len and data:sub(pos, pos + 4) == "false" then
|
||||||
pos = pos + 5
|
pos = pos + 5
|
||||||
return false
|
return false
|
||||||
elseif b == b_minus or (b >= b_0 and b <= b_9) then
|
elseif b == b_minus or (b >= b_0 and b <= b_9) then
|
||||||
return parse_number()
|
return parse_number()
|
||||||
else
|
else
|
||||||
error("Unexpected character: " .. string.char(b))
|
error("Unexpected character: " .. string.char(b))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
skip()
|
skip()
|
||||||
@ -366,68 +357,66 @@ function json.decode(data)
|
|||||||
skip()
|
skip()
|
||||||
|
|
||||||
if pos <= len then
|
if pos <= len then
|
||||||
error("Unexpected trailing characters")
|
error("Unexpected trailing characters")
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.is_valid(str)
|
function json_is_valid(str)
|
||||||
if type(str) ~= "string" then return false end
|
if type(str) ~= "string" then return false end
|
||||||
local status, _ = pcall(json.decode, str)
|
local status, _ = pcall(json_decode, str)
|
||||||
return status
|
return status
|
||||||
end
|
end
|
||||||
|
|
||||||
function json.pretty_print(value)
|
function json_pretty_print(value)
|
||||||
if type(value) == "string" then
|
if type(value) == "string" then
|
||||||
value = json.decode(value)
|
value = json_decode(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function stringify(val, indent, visited)
|
local function stringify(val, indent, visited)
|
||||||
visited = visited or {}
|
visited = visited or {}
|
||||||
indent = indent or 0
|
indent = indent or 0
|
||||||
local spaces = string.rep(" ", indent)
|
local spaces = string.rep(" ", indent)
|
||||||
|
|
||||||
if type(val) == "table" then
|
if type(val) == "table" then
|
||||||
if visited[val] then return "{...}" end
|
if visited[val] then return "{...}" end
|
||||||
visited[val] = true
|
visited[val] = true
|
||||||
|
|
||||||
local isArray = true
|
local isArray = true
|
||||||
local i = 1
|
local i = 1
|
||||||
for k in pairs(val) do
|
for k in pairs(val) do
|
||||||
if type(k) ~= "number" or k ~= i then
|
if type(k) ~= "number" or k ~= i then
|
||||||
isArray = false
|
isArray = false
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = isArray and "[\n" or "{\n"
|
local result = isArray and "[\n" or "{\n"
|
||||||
local first = true
|
local first = true
|
||||||
|
|
||||||
if isArray then
|
if isArray then
|
||||||
for i, v in ipairs(val) do
|
for i, v in ipairs(val) do
|
||||||
if not first then result = result .. ",\n" end
|
if not first then result = result .. ",\n" end
|
||||||
first = false
|
first = false
|
||||||
result = result .. spaces .. " " .. stringify(v, indent + 1, visited)
|
result = result .. spaces .. " " .. stringify(v, indent + 1, visited)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for k, v in pairs(val) do
|
for k, v in pairs(val) do
|
||||||
if not first then result = result .. ",\n" end
|
if not first then result = result .. ",\n" end
|
||||||
first = false
|
first = false
|
||||||
result = result .. spaces .. " \"" .. tostring(k) .. "\": " .. stringify(v, indent + 1, visited)
|
result = result .. spaces .. " \"" .. tostring(k) .. "\": " .. stringify(v, indent + 1, visited)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return result .. "\n" .. spaces .. (isArray and "]" or "}")
|
return result .. "\n" .. spaces .. (isArray and "]" or "}")
|
||||||
elseif type(val) == "string" then
|
elseif type(val) == "string" then
|
||||||
return "\"" .. val:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. "\""
|
return "\"" .. val:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. "\""
|
||||||
else
|
else
|
||||||
return tostring(val)
|
return tostring(val)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return stringify(value)
|
return stringify(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
return json
|
|
@ -60,126 +60,121 @@ function __ensure_response()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- HTTP MODULE
|
-- HTTP FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
local http = {
|
-- Set HTTP status code
|
||||||
-- Set HTTP status code
|
function http_set_status(code)
|
||||||
set_status = function(code)
|
if type(code) ~= "number" then
|
||||||
if type(code) ~= "number" then
|
error("http_set_status: status code must be a number", 2)
|
||||||
error("http.set_status: status code must be a number", 2)
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.status = code
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set HTTP header
|
||||||
|
function http_set_header(name, value)
|
||||||
|
if type(name) ~= "string" or type(value) ~= "string" then
|
||||||
|
error("http_set_header: name and value must be strings", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.headers = resp.headers or {}
|
||||||
|
resp.headers[name] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set content type; http_set_header helper
|
||||||
|
function http_set_content_type(content_type)
|
||||||
|
http_set_header("Content-Type", content_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set metadata (arbitrary data to be returned with response)
|
||||||
|
function http_set_metadata(key, value)
|
||||||
|
if type(key) ~= "string" then
|
||||||
|
error("http_set_metadata: key must be a string", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.metadata = resp.metadata or {}
|
||||||
|
resp.metadata[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generic HTTP request function
|
||||||
|
function http_request(method, url, body, options)
|
||||||
|
if type(method) ~= "string" then
|
||||||
|
error("http_request: method must be a string", 2)
|
||||||
|
end
|
||||||
|
if type(url) ~= "string" then
|
||||||
|
error("http_request: url must be a string", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call native implementation
|
||||||
|
local result = __http_request(method, url, body, options)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shorthand function to directly get JSON
|
||||||
|
function http_get_json(url, options)
|
||||||
|
options = options or {}
|
||||||
|
local response = http_get(url, options)
|
||||||
|
if response.ok and response.json then
|
||||||
|
return response.json
|
||||||
|
end
|
||||||
|
return nil, response
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Utility to build a URL with query parameters
|
||||||
|
function http_build_url(base_url, params)
|
||||||
|
if not params or type(params) ~= "table" then
|
||||||
|
return base_url
|
||||||
|
end
|
||||||
|
|
||||||
|
local query = {}
|
||||||
|
for k, v in pairs(params) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
for _, item in ipairs(v) do
|
||||||
|
table.insert(query, url_encode(k) .. "=" .. url_encode(tostring(item)))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(query, url_encode(k) .. "=" .. url_encode(tostring(v)))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local resp = __ensure_response()
|
if #query > 0 then
|
||||||
resp.status = code
|
if string.contains(base_url, "?") then
|
||||||
end,
|
return base_url .. "&" .. table.concat(query, "&")
|
||||||
|
else
|
||||||
-- Set HTTP header
|
return base_url .. "?" .. table.concat(query, "&")
|
||||||
set_header = function(name, value)
|
|
||||||
if type(name) ~= "string" or type(value) ~= "string" then
|
|
||||||
error("http.set_header: name and value must be strings", 2)
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local resp = __ensure_response()
|
return base_url
|
||||||
resp.headers = resp.headers or {}
|
end
|
||||||
resp.headers[name] = value
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Set content type; set_header helper
|
|
||||||
set_content_type = function(content_type)
|
|
||||||
http.set_header("Content-Type", content_type)
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Set metadata (arbitrary data to be returned with response)
|
|
||||||
set_metadata = function(key, value)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("http.set_metadata: key must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resp = __ensure_response()
|
|
||||||
resp.metadata = resp.metadata or {}
|
|
||||||
resp.metadata[key] = value
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- HTTP client submodule
|
|
||||||
client = {
|
|
||||||
-- Generic request function
|
|
||||||
request = function(method, url, body, options)
|
|
||||||
if type(method) ~= "string" then
|
|
||||||
error("http.client.request: method must be a string", 2)
|
|
||||||
end
|
|
||||||
if type(url) ~= "string" then
|
|
||||||
error("http.client.request: url must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Call native implementation
|
|
||||||
local result = __http_request(method, url, body, options)
|
|
||||||
return result
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Shorthand function to directly get JSON
|
|
||||||
get_json = function(url, options)
|
|
||||||
options = options or {}
|
|
||||||
local response = http.client.get(url, options)
|
|
||||||
if response.ok and response.json then
|
|
||||||
return response.json
|
|
||||||
end
|
|
||||||
return nil, response
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Utility to build a URL with query parameters
|
|
||||||
build_url = function(base_url, params)
|
|
||||||
if not params or type(params) ~= "table" then
|
|
||||||
return base_url
|
|
||||||
end
|
|
||||||
|
|
||||||
local query = {}
|
|
||||||
for k, v in pairs(params) do
|
|
||||||
if type(v) == "table" then
|
|
||||||
for _, item in ipairs(v) do
|
|
||||||
table.insert(query, util.url_encode(k) .. "=" .. util.url_encode(tostring(item)))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(query, util.url_encode(k) .. "=" .. util.url_encode(tostring(v)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #query > 0 then
|
|
||||||
if string.contains(base_url, "?") then
|
|
||||||
return base_url .. "&" .. table.concat(query, "&")
|
|
||||||
else
|
|
||||||
return base_url .. "?" .. table.concat(query, "&")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return base_url
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local function make_method(method, needs_body)
|
local function make_method(method, needs_body)
|
||||||
return function(url, body_or_options, options)
|
return function(url, body_or_options, options)
|
||||||
if needs_body then
|
if needs_body then
|
||||||
options = options or {}
|
options = options or {}
|
||||||
return http.client.request(method, url, body_or_options, options)
|
return http_request(method, url, body_or_options, options)
|
||||||
else
|
else
|
||||||
body_or_options = body_or_options or {}
|
body_or_options = body_or_options or {}
|
||||||
return http.client.request(method, url, nil, body_or_options)
|
return http_request(method, url, nil, body_or_options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
http.client.get = make_method("GET", false)
|
http_get = make_method("GET", false)
|
||||||
http.client.delete = make_method("DELETE", false)
|
http_delete = make_method("DELETE", false)
|
||||||
http.client.head = make_method("HEAD", false)
|
http_head = make_method("HEAD", false)
|
||||||
http.client.options = make_method("OPTIONS", false)
|
http_options = make_method("OPTIONS", false)
|
||||||
http.client.post = make_method("POST", true)
|
http_post = make_method("POST", true)
|
||||||
http.client.put = make_method("PUT", true)
|
http_put = make_method("PUT", true)
|
||||||
http.client.patch = make_method("PATCH", true)
|
http_patch = make_method("PATCH", true)
|
||||||
|
|
||||||
http.redirect = function(url, status)
|
function http_redirect(url, status)
|
||||||
if type(url) ~= "string" then
|
if type(url) ~= "string" then
|
||||||
error("http.redirect: url must be a string", 2)
|
error("http_redirect: url must be a string", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
status = status or 302 -- Default to temporary redirect
|
status = status or 302 -- Default to temporary redirect
|
||||||
@ -194,228 +189,227 @@ http.redirect = function(url, status)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- COOKIE MODULE
|
-- COOKIE FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
local cookie = {
|
-- Set a cookie
|
||||||
-- Set a cookie
|
function cookie_set(name, value, options)
|
||||||
set = function(name, value, options)
|
if type(name) ~= "string" then
|
||||||
if type(name) ~= "string" then
|
error("cookie_set: name must be a string", 2)
|
||||||
error("cookie.set: name must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resp = __ensure_response()
|
|
||||||
resp.cookies = resp.cookies or {}
|
|
||||||
|
|
||||||
local opts = options or {}
|
|
||||||
local cookie = {
|
|
||||||
name = name,
|
|
||||||
value = value or "",
|
|
||||||
path = opts.path or "/",
|
|
||||||
domain = opts.domain
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.expires then
|
|
||||||
if type(opts.expires) == "number" then
|
|
||||||
if opts.expires > 0 then
|
|
||||||
cookie.max_age = opts.expires
|
|
||||||
local now = os.time()
|
|
||||||
cookie.expires = now + opts.expires
|
|
||||||
elseif opts.expires < 0 then
|
|
||||||
cookie.expires = 1
|
|
||||||
cookie.max_age = 0
|
|
||||||
end
|
|
||||||
-- opts.expires == 0: Session cookie (omitting both expires and max-age)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cookie.secure = (opts.secure ~= false)
|
|
||||||
cookie.http_only = (opts.http_only ~= false)
|
|
||||||
|
|
||||||
if opts.same_site then
|
|
||||||
local same_site = string.trim(opts.same_site):lower()
|
|
||||||
local valid_values = {none = true, lax = true, strict = true}
|
|
||||||
|
|
||||||
if not valid_values[same_site] then
|
|
||||||
error("cookie.set: same_site must be one of 'None', 'Lax', or 'Strict'", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If SameSite=None, the cookie must be secure
|
|
||||||
if same_site == "none" and not cookie.secure then
|
|
||||||
cookie.secure = true
|
|
||||||
end
|
|
||||||
|
|
||||||
cookie.same_site = opts.same_site
|
|
||||||
else
|
|
||||||
cookie.same_site = "Lax"
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(resp.cookies, cookie)
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Get a cookie value
|
|
||||||
get = function(name)
|
|
||||||
if type(name) ~= "string" then
|
|
||||||
error("cookie.get: name must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local env = getfenv(2)
|
|
||||||
|
|
||||||
if env.ctx and env.ctx.cookies then
|
|
||||||
return env.ctx.cookies[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
if env.ctx and env.ctx._request_cookies then
|
|
||||||
return env.ctx._request_cookies[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Remove a cookie
|
|
||||||
remove = function(name, path, domain)
|
|
||||||
if type(name) ~= "string" then
|
|
||||||
error("cookie.remove: name must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
|
|
||||||
end
|
end
|
||||||
}
|
|
||||||
|
|
||||||
-- ======================================================================
|
local resp = __ensure_response()
|
||||||
-- SESSION MODULE
|
resp.cookies = resp.cookies or {}
|
||||||
-- ======================================================================
|
|
||||||
|
|
||||||
local session = {
|
local opts = options or {}
|
||||||
get = function(key)
|
local cookie = {
|
||||||
if type(key) ~= "string" then
|
name = name,
|
||||||
error("session.get: key must be a string", 2)
|
value = value or "",
|
||||||
end
|
path = opts.path or "/",
|
||||||
|
domain = opts.domain
|
||||||
|
}
|
||||||
|
|
||||||
local env = getfenv(2)
|
if opts.expires then
|
||||||
|
if type(opts.expires) == "number" then
|
||||||
if env.ctx and env.ctx.session and env.ctx.session.data then
|
if opts.expires > 0 then
|
||||||
return env.ctx.session.data[key]
|
cookie.max_age = opts.expires
|
||||||
end
|
local now = os.time()
|
||||||
|
cookie.expires = now + opts.expires
|
||||||
return nil
|
elseif opts.expires < 0 then
|
||||||
end,
|
cookie.expires = 1
|
||||||
|
cookie.max_age = 0
|
||||||
set = function(key, value)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.set: key must be a string", 2)
|
|
||||||
end
|
|
||||||
if type(value) == nil then
|
|
||||||
error("session.set: value cannot be nil", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resp = __ensure_response()
|
|
||||||
resp.session = resp.session or {}
|
|
||||||
resp.session[key] = value
|
|
||||||
end,
|
|
||||||
|
|
||||||
id = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
|
|
||||||
if env.ctx and env.ctx.session then
|
|
||||||
return env.ctx.session.id
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
|
|
||||||
get_all = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
|
|
||||||
if env.ctx and env.ctx.session then
|
|
||||||
return env.ctx.session.data
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
|
|
||||||
delete = function(key)
|
|
||||||
if type(key) ~= "string" then
|
|
||||||
error("session.delete: key must be a string", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local resp = __ensure_response()
|
|
||||||
resp.session = resp.session or {}
|
|
||||||
resp.session[key] = "__SESSION_DELETE_MARKER__"
|
|
||||||
|
|
||||||
local env = getfenv(2)
|
|
||||||
if env.ctx and env.ctx.session and env.ctx.session.data then
|
|
||||||
env.ctx.session.data[key] = nil
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
clear = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
if env.ctx and env.ctx.session and env.ctx.session.data then
|
|
||||||
for k, _ in pairs(env.ctx.session.data) do
|
|
||||||
env.ctx.session.data[k] = nil
|
|
||||||
end
|
end
|
||||||
|
-- opts.expires == 0: Session cookie (omitting both expires and max-age)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cookie.secure = (opts.secure ~= false)
|
||||||
|
cookie.http_only = (opts.http_only ~= false)
|
||||||
|
|
||||||
|
if opts.same_site then
|
||||||
|
local same_site = string.trim(opts.same_site):lower()
|
||||||
|
local valid_values = {none = true, lax = true, strict = true}
|
||||||
|
|
||||||
|
if not valid_values[same_site] then
|
||||||
|
error("cookie_set: same_site must be one of 'None', 'Lax', or 'Strict'", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local resp = __ensure_response()
|
-- If SameSite=None, the cookie must be secure
|
||||||
resp.session = {}
|
if same_site == "none" and not cookie.secure then
|
||||||
resp.session["__clear_all"] = true
|
cookie.secure = true
|
||||||
|
end
|
||||||
|
|
||||||
|
cookie.same_site = opts.same_site
|
||||||
|
else
|
||||||
|
cookie.same_site = "Lax"
|
||||||
end
|
end
|
||||||
}
|
|
||||||
|
table.insert(resp.cookies, cookie)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get a cookie value
|
||||||
|
function cookie_get(name)
|
||||||
|
if type(name) ~= "string" then
|
||||||
|
error("cookie_get: name must be a string", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local env = getfenv(2)
|
||||||
|
|
||||||
|
if env.ctx and env.ctx.cookies then
|
||||||
|
return env.ctx.cookies[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
if env.ctx and env.ctx._request_cookies then
|
||||||
|
return env.ctx._request_cookies[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove a cookie
|
||||||
|
function cookie_remove(name, path, domain)
|
||||||
|
if type(name) ~= "string" then
|
||||||
|
error("cookie_remove: name must be a string", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return cookie_set(name, "", {expires = 0, path = path or "/", domain = domain})
|
||||||
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- CSRF MODULE
|
-- SESSION FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
local csrf = {
|
function session_get(key)
|
||||||
generate = function()
|
if type(key) ~= "string" then
|
||||||
local token = util.generate_token(32)
|
error("session_get: key must be a string", 2)
|
||||||
session.set("_csrf_token", token)
|
|
||||||
return token
|
|
||||||
end,
|
|
||||||
|
|
||||||
field = function()
|
|
||||||
local token = session.get("_csrf_token")
|
|
||||||
if not token then
|
|
||||||
token = csrf.generate()
|
|
||||||
end
|
|
||||||
return string.format('<input type="hidden" name="_csrf_token" value="%s" />',
|
|
||||||
util.html_special_chars(token))
|
|
||||||
end,
|
|
||||||
|
|
||||||
validate = function()
|
|
||||||
local env = getfenv(2)
|
|
||||||
local token = false
|
|
||||||
if env.ctx and env.ctx.session and env.ctx.session.data then
|
|
||||||
token = env.ctx.session.data["_csrf_token"]
|
|
||||||
end
|
|
||||||
|
|
||||||
if not token then
|
|
||||||
http.set_status(403)
|
|
||||||
__http_response.body = "CSRF validation failed"
|
|
||||||
exit()
|
|
||||||
end
|
|
||||||
|
|
||||||
local request_token = nil
|
|
||||||
if env.ctx and env.ctx.form then
|
|
||||||
request_token = env.ctx.form._csrf_token
|
|
||||||
end
|
|
||||||
|
|
||||||
if not request_token and env.ctx and env.ctx._request_headers then
|
|
||||||
request_token = env.ctx._request_headers["x-csrf-token"] or
|
|
||||||
env.ctx._request_headers["csrf-token"]
|
|
||||||
end
|
|
||||||
|
|
||||||
if not request_token or request_token ~= token then
|
|
||||||
http.set_status(403)
|
|
||||||
__http_response.body = "CSRF validation failed"
|
|
||||||
exit()
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
}
|
|
||||||
|
local env = getfenv(2)
|
||||||
|
|
||||||
|
if env.ctx and env.ctx.session and env.ctx.session.data then
|
||||||
|
return env.ctx.session.data[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function session_set(key, value)
|
||||||
|
if type(key) ~= "string" then
|
||||||
|
error("session_set: key must be a string", 2)
|
||||||
|
end
|
||||||
|
if type(value) == nil then
|
||||||
|
error("session_set: value cannot be nil", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.session = resp.session or {}
|
||||||
|
resp.session[key] = value
|
||||||
|
|
||||||
|
local env = getfenv(2)
|
||||||
|
if env.ctx and env.ctx.session and env.ctx.session.data then
|
||||||
|
env.ctx.session.data[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function session_id()
|
||||||
|
local env = getfenv(2)
|
||||||
|
|
||||||
|
if env.ctx and env.ctx.session then
|
||||||
|
return env.ctx.session.id
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function session_get_all()
|
||||||
|
local env = getfenv(2)
|
||||||
|
|
||||||
|
if env.ctx and env.ctx.session then
|
||||||
|
return env.ctx.session.data
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function session_delete(key)
|
||||||
|
if type(key) ~= "string" then
|
||||||
|
error("session_delete: key must be a string", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.session = resp.session or {}
|
||||||
|
resp.session[key] = "__SESSION_DELETE_MARKER__"
|
||||||
|
|
||||||
|
local env = getfenv(2)
|
||||||
|
if env.ctx and env.ctx.session and env.ctx.session.data then
|
||||||
|
env.ctx.session.data[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function session_clear()
|
||||||
|
local env = getfenv(2)
|
||||||
|
if env.ctx and env.ctx.session and env.ctx.session.data then
|
||||||
|
for k, _ in pairs(env.ctx.session.data) do
|
||||||
|
env.ctx.session.data[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local resp = __ensure_response()
|
||||||
|
resp.session = {}
|
||||||
|
resp.session["__clear_all"] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- CSRF FUNCTIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
function csrf_generate()
|
||||||
|
local token = generate_token(32)
|
||||||
|
session_set("_csrf_token", token)
|
||||||
|
return token
|
||||||
|
end
|
||||||
|
|
||||||
|
function csrf_field()
|
||||||
|
local token = session_get("_csrf_token")
|
||||||
|
if not token then
|
||||||
|
token = csrf_generate()
|
||||||
|
end
|
||||||
|
return string.format('<input type="hidden" name="_csrf_token" value="%s" />',
|
||||||
|
html_special_chars(token))
|
||||||
|
end
|
||||||
|
|
||||||
|
function csrf_validate()
|
||||||
|
local env = getfenv(2)
|
||||||
|
local token = false
|
||||||
|
if env.ctx and env.ctx.session and env.ctx.session.data then
|
||||||
|
token = env.ctx.session.data["_csrf_token"]
|
||||||
|
end
|
||||||
|
|
||||||
|
if not token then
|
||||||
|
http_set_status(403)
|
||||||
|
__http_response.body = "CSRF validation failed"
|
||||||
|
exit()
|
||||||
|
end
|
||||||
|
|
||||||
|
local request_token = nil
|
||||||
|
if env.ctx and env.ctx.form then
|
||||||
|
request_token = env.ctx.form._csrf_token
|
||||||
|
end
|
||||||
|
|
||||||
|
if not request_token and env.ctx and env.ctx._request_headers then
|
||||||
|
request_token = env.ctx._request_headers["x-csrf-token"] or
|
||||||
|
env.ctx._request_headers["csrf-token"]
|
||||||
|
end
|
||||||
|
|
||||||
|
if not request_token or request_token ~= token then
|
||||||
|
http_set_status(403)
|
||||||
|
__http_response.body = "CSRF validation failed"
|
||||||
|
exit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- TEMPLATE RENDER FUNCTIONS
|
-- TEMPLATE RENDER FUNCTIONS
|
||||||
@ -502,7 +496,7 @@ _G.render = function(template_str, env)
|
|||||||
setfenv(fn, runtime_env)
|
setfenv(fn, runtime_env)
|
||||||
|
|
||||||
local output_buffer = {}
|
local output_buffer = {}
|
||||||
fn(tostring, util.html_special_chars, output_buffer, 0)
|
fn(tostring, html_special_chars, output_buffer, 0)
|
||||||
return table.concat(output_buffer)
|
return table.concat(output_buffer)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -536,7 +530,7 @@ _G.parse = function(template_str, env)
|
|||||||
local value = env[name]
|
local value = env[name]
|
||||||
local str = tostring(value or "")
|
local str = tostring(value or "")
|
||||||
if escaped then
|
if escaped then
|
||||||
str = util.html_special_chars(str)
|
str = html_special_chars(str)
|
||||||
end
|
end
|
||||||
table.insert(output, str)
|
table.insert(output, str)
|
||||||
|
|
||||||
@ -576,7 +570,7 @@ _G.iparse = function(template_str, values)
|
|||||||
local value = values[value_index]
|
local value = values[value_index]
|
||||||
local str = tostring(value or "")
|
local str = tostring(value or "")
|
||||||
if escaped then
|
if escaped then
|
||||||
str = util.html_special_chars(str)
|
str = html_special_chars(str)
|
||||||
end
|
end
|
||||||
table.insert(output, str)
|
table.insert(output, str)
|
||||||
|
|
||||||
@ -588,11 +582,9 @@ _G.iparse = function(template_str, values)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- PASSWORD MODULE
|
-- PASSWORD FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
local password = {}
|
|
||||||
|
|
||||||
-- Hash a password using Argon2id
|
-- Hash a password using Argon2id
|
||||||
-- Options:
|
-- Options:
|
||||||
-- memory: Amount of memory to use in KB (default: 128MB)
|
-- memory: Amount of memory to use in KB (default: 128MB)
|
||||||
@ -600,85 +592,72 @@ local password = {}
|
|||||||
-- parallelism: Number of threads (default: 4)
|
-- parallelism: Number of threads (default: 4)
|
||||||
-- salt_length: Length of salt in bytes (default: 16)
|
-- salt_length: Length of salt in bytes (default: 16)
|
||||||
-- key_length: Length of the derived key in bytes (default: 32)
|
-- key_length: Length of the derived key in bytes (default: 32)
|
||||||
function password.hash(plain_password, options)
|
function password_hash(plain_password, options)
|
||||||
if type(plain_password) ~= "string" then
|
if type(plain_password) ~= "string" then
|
||||||
error("password.hash: expected string password", 2)
|
error("password_hash: expected string password", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
return __password_hash(plain_password, options)
|
return __password_hash(plain_password, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Verify a password against a hash
|
-- Verify a password against a hash
|
||||||
function password.verify(plain_password, hash_string)
|
function password_verify(plain_password, hash_string)
|
||||||
if type(plain_password) ~= "string" then
|
if type(plain_password) ~= "string" then
|
||||||
error("password.verify: expected string password", 2)
|
error("password_verify: expected string password", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
if type(hash_string) ~= "string" then
|
if type(hash_string) ~= "string" then
|
||||||
error("password.verify: expected string hash", 2)
|
error("password_verify: expected string hash", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
return __password_verify(plain_password, hash_string)
|
return __password_verify(plain_password, hash_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- SEND MODULE
|
-- SEND FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
local send = {}
|
function send_html(content)
|
||||||
|
http_set_content_type("text/html")
|
||||||
function send.html(content)
|
|
||||||
http.set_content_type("text/html")
|
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.json(content)
|
function send_json(content)
|
||||||
http.set_content_type("application/json")
|
http_set_content_type("application/json")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.text(content)
|
function send_text(content)
|
||||||
http.set_content_type("text/plain")
|
http_set_content_type("text/plain")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.xml(content)
|
function send_xml(content)
|
||||||
http.set_content_type("application/xml")
|
http_set_content_type("application/xml")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.javascript(content)
|
function send_javascript(content)
|
||||||
http.set_content_type("application/javascript")
|
http_set_content_type("application/javascript")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.css(content)
|
function send_css(content)
|
||||||
http.set_content_type("text/css")
|
http_set_content_type("text/css")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.svg(content)
|
function send_svg(content)
|
||||||
http.set_content_type("image/svg+xml")
|
http_set_content_type("image/svg+xml")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.csv(content)
|
function send_csv(content)
|
||||||
http.set_content_type("text/csv")
|
http_set_content_type("text/csv")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
function send.binary(content, mime_type)
|
function send_binary(content, mime_type)
|
||||||
http.set_content_type(mime_type or "application/octet-stream")
|
http_set_content_type(mime_type or "application/octet-stream")
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
|
||||||
-- REGISTER MODULES GLOBALLY
|
|
||||||
-- ======================================================================
|
|
||||||
|
|
||||||
_G.http = http
|
|
||||||
_G.session = session
|
|
||||||
_G.csrf = csrf
|
|
||||||
_G.cookie = cookie
|
|
||||||
_G.password = password
|
|
||||||
_G.send = send
|
|
@ -1,16 +1,13 @@
|
|||||||
--[[
|
--[[
|
||||||
util.lua - Utility functions for the Lua sandbox
|
util.lua - Utility functions for the Lua sandbox
|
||||||
Enhanced with web development utilities
|
|
||||||
]]--
|
]]--
|
||||||
|
|
||||||
local util = {}
|
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- CORE UTILITY FUNCTIONS
|
-- CORE UTILITY FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- Generate a random token
|
-- Generate a random token
|
||||||
function util.generate_token(length)
|
function generate_token(length)
|
||||||
return __generate_token(length or 32)
|
return __generate_token(length or 32)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -18,20 +15,8 @@ end
|
|||||||
-- HTML ENTITY FUNCTIONS
|
-- HTML ENTITY FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- HTML entity mapping for common characters
|
|
||||||
local html_entities = {
|
|
||||||
["&"] = "&",
|
|
||||||
["<"] = "<",
|
|
||||||
[">"] = ">",
|
|
||||||
['"'] = """,
|
|
||||||
["'"] = "'",
|
|
||||||
["/"] = "/",
|
|
||||||
["`"] = "`",
|
|
||||||
["="] = "="
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Convert special characters to HTML entities (like htmlspecialchars)
|
-- Convert special characters to HTML entities (like htmlspecialchars)
|
||||||
function util.html_special_chars(str)
|
function html_special_chars(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -40,7 +25,7 @@ function util.html_special_chars(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Convert all applicable characters to HTML entities (like htmlentities)
|
-- Convert all applicable characters to HTML entities (like htmlentities)
|
||||||
function util.html_entities(str)
|
function html_entities(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -49,7 +34,7 @@ function util.html_entities(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Convert HTML entities back to characters (simple version)
|
-- Convert HTML entities back to characters (simple version)
|
||||||
function util.html_entity_decode(str)
|
function html_entity_decode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -64,7 +49,7 @@ function util.html_entity_decode(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Convert newlines to <br> tags
|
-- Convert newlines to <br> tags
|
||||||
function util.nl2br(str)
|
function nl2br(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -77,7 +62,7 @@ end
|
|||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- URL encode a string
|
-- URL encode a string
|
||||||
function util.url_encode(str)
|
function url_encode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -91,7 +76,7 @@ function util.url_encode(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- URL decode a string
|
-- URL decode a string
|
||||||
function util.url_decode(str)
|
function url_decode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -108,7 +93,7 @@ end
|
|||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- Email validation
|
-- Email validation
|
||||||
function util.is_email(str)
|
function is_email(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -119,7 +104,7 @@ function util.is_email(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- URL validation
|
-- URL validation
|
||||||
function util.is_url(str)
|
function is_url(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -130,7 +115,7 @@ function util.is_url(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- IP address validation (IPv4)
|
-- IP address validation (IPv4)
|
||||||
function util.is_ipv4(str)
|
function is_ipv4(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -147,7 +132,7 @@ function util.is_ipv4(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Integer validation
|
-- Integer validation
|
||||||
function util.is_int(str)
|
function is_int(str)
|
||||||
if type(str) == "number" then
|
if type(str) == "number" then
|
||||||
return math.floor(str) == str
|
return math.floor(str) == str
|
||||||
elseif type(str) ~= "string" then
|
elseif type(str) ~= "string" then
|
||||||
@ -158,7 +143,7 @@ function util.is_int(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Float validation
|
-- Float validation
|
||||||
function util.is_float(str)
|
function is_float(str)
|
||||||
if type(str) == "number" then
|
if type(str) == "number" then
|
||||||
return true
|
return true
|
||||||
elseif type(str) ~= "string" then
|
elseif type(str) ~= "string" then
|
||||||
@ -169,7 +154,7 @@ function util.is_float(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Boolean validation
|
-- Boolean validation
|
||||||
function util.is_bool(value)
|
function is_bool(value)
|
||||||
if type(value) == "boolean" then
|
if type(value) == "boolean" then
|
||||||
return true
|
return true
|
||||||
elseif type(value) ~= "string" and type(value) ~= "number" then
|
elseif type(value) ~= "string" and type(value) ~= "number" then
|
||||||
@ -183,7 +168,7 @@ function util.is_bool(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Convert to boolean
|
-- Convert to boolean
|
||||||
function util.to_bool(value)
|
function to_bool(value)
|
||||||
if type(value) == "boolean" then
|
if type(value) == "boolean" then
|
||||||
return value
|
return value
|
||||||
elseif type(value) ~= "string" and type(value) ~= "number" then
|
elseif type(value) ~= "string" and type(value) ~= "number" then
|
||||||
@ -195,16 +180,16 @@ function util.to_bool(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Sanitize string (simple version)
|
-- Sanitize string (simple version)
|
||||||
function util.sanitize_string(str)
|
function sanitize_string(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
|
|
||||||
return util.html_special_chars(str)
|
return html_special_chars(str)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sanitize to integer
|
-- Sanitize to integer
|
||||||
function util.sanitize_int(value)
|
function sanitize_int(value)
|
||||||
if type(value) ~= "string" and type(value) ~= "number" then
|
if type(value) ~= "string" and type(value) ~= "number" then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
@ -215,7 +200,7 @@ function util.sanitize_int(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Sanitize to float
|
-- Sanitize to float
|
||||||
function util.sanitize_float(value)
|
function sanitize_float(value)
|
||||||
if type(value) ~= "string" and type(value) ~= "number" then
|
if type(value) ~= "string" and type(value) ~= "number" then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
@ -226,7 +211,7 @@ function util.sanitize_float(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Sanitize URL
|
-- Sanitize URL
|
||||||
function util.sanitize_url(str)
|
function sanitize_url(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@ -235,12 +220,12 @@ function util.sanitize_url(str)
|
|||||||
str = str:gsub("[\000-\031]", "")
|
str = str:gsub("[\000-\031]", "")
|
||||||
|
|
||||||
-- Make sure it's a valid URL
|
-- Make sure it's a valid URL
|
||||||
if util.is_url(str) then
|
if is_url(str) then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Try to prepend http:// if it's missing
|
-- Try to prepend http:// if it's missing
|
||||||
if not str:match("^https?://") and util.is_url("http://" .. str) then
|
if not str:match("^https?://") and is_url("http://" .. str) then
|
||||||
return "http://" .. str
|
return "http://" .. str
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -248,7 +233,7 @@ function util.sanitize_url(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Sanitize email
|
-- Sanitize email
|
||||||
function util.sanitize_email(str)
|
function sanitize_email(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@ -257,7 +242,7 @@ function util.sanitize_email(str)
|
|||||||
str = str:gsub("[^%a%d%!%#%$%%%&%'%*%+%-%/%=%?%^%_%`%{%|%}%~%@%.%[%]]", "")
|
str = str:gsub("[^%a%d%!%#%$%%%&%'%*%+%-%/%=%?%^%_%`%{%|%}%~%@%.%[%]]", "")
|
||||||
|
|
||||||
-- Return only if it's a valid email
|
-- Return only if it's a valid email
|
||||||
if util.is_email(str) then
|
if is_email(str) then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -269,13 +254,13 @@ end
|
|||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
-- Basic XSS prevention
|
-- Basic XSS prevention
|
||||||
function util.xss_clean(str)
|
function xss_clean(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Convert problematic characters to entities
|
-- Convert problematic characters to entities
|
||||||
local result = util.html_special_chars(str)
|
local result = html_special_chars(str)
|
||||||
|
|
||||||
-- Remove JavaScript event handlers
|
-- Remove JavaScript event handlers
|
||||||
result = result:gsub("on%w+%s*=", "")
|
result = result:gsub("on%w+%s*=", "")
|
||||||
@ -290,7 +275,7 @@ function util.xss_clean(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Base64 encode
|
-- Base64 encode
|
||||||
function util.base64_encode(str)
|
function base64_encode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
@ -299,12 +284,10 @@ function util.base64_encode(str)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Base64 decode
|
-- Base64 decode
|
||||||
function util.base64_decode(str)
|
function base64_decode(str)
|
||||||
if type(str) ~= "string" then
|
if type(str) ~= "string" then
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
return __base64_decode(str)
|
return __base64_decode(str)
|
||||||
end
|
end
|
||||||
|
|
||||||
return util
|
|
@ -81,6 +81,9 @@ func (s *Session) GetAll() map[string]any {
|
|||||||
|
|
||||||
// Set stores a value in the session
|
// Set stores a value in the session
|
||||||
func (s *Session) Set(key string, value any) {
|
func (s *Session) Set(key string, value any) {
|
||||||
|
if existing, ok := s.Data[key]; ok && deepEqual(existing, value) {
|
||||||
|
return // No change
|
||||||
|
}
|
||||||
s.Data[key] = value
|
s.Data[key] = value
|
||||||
s.UpdatedAt = time.Now()
|
s.UpdatedAt = time.Now()
|
||||||
s.dirty = true
|
s.dirty = true
|
||||||
@ -346,3 +349,81 @@ func validate(v any) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deepEqual efficiently compares two values for deep equality
|
||||||
|
func deepEqual(a, b any) bool {
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch va := a.(type) {
|
||||||
|
case string:
|
||||||
|
if vb, ok := b.(string); ok {
|
||||||
|
return va == vb
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if vb, ok := b.(int); ok {
|
||||||
|
return va == vb
|
||||||
|
}
|
||||||
|
if vb, ok := b.(int64); ok {
|
||||||
|
return int64(va) == vb
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
if vb, ok := b.(int64); ok {
|
||||||
|
return va == vb
|
||||||
|
}
|
||||||
|
if vb, ok := b.(int); ok {
|
||||||
|
return va == int64(vb)
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
if vb, ok := b.(float64); ok {
|
||||||
|
return va == vb
|
||||||
|
}
|
||||||
|
case bool:
|
||||||
|
if vb, ok := b.(bool); ok {
|
||||||
|
return va == vb
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
if vb, ok := b.([]byte); ok {
|
||||||
|
if len(va) != len(vb) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range va {
|
||||||
|
if v != vb[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
if vb, ok := b.(map[string]any); ok {
|
||||||
|
if len(va) != len(vb) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range va {
|
||||||
|
if bv, exists := vb[k]; !exists || !deepEqual(v, bv) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
if vb, ok := b.([]any); ok {
|
||||||
|
if len(va) != len(vb) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range va {
|
||||||
|
if !deepEqual(v, vb[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user