diff --git a/resty/core.lua b/resty/core.lua new file mode 100644 index 0000000..54d9756 --- /dev/null +++ b/resty/core.lua @@ -0,0 +1,34 @@ +-- Copyright (C) Yichun Zhang (agentzh) + +local subsystem = ngx.config.subsystem + + +require "resty.core.var" +require "resty.core.worker" +require "resty.core.regex" +require "resty.core.shdict" +require "resty.core.time" +require "resty.core.hash" +require "resty.core.uri" +require "resty.core.exit" +require "resty.core.base64" + + +if subsystem == 'http' then + require "resty.core.request" + require "resty.core.response" + require "resty.core.phase" + require "resty.core.ndk" +end + + +require "resty.core.misc" +require "resty.core.ctx" + + +local base = require "resty.core.base" + + +return { + version = base.version +} diff --git a/resty/core/base.lua b/resty/core/base.lua new file mode 100644 index 0000000..0b27d4a --- /dev/null +++ b/resty/core/base.lua @@ -0,0 +1,259 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local ffi_new = ffi.new +local error = error +local select = select +local ceil = math.ceil +local subsystem = ngx.config.subsystem + + +local str_buf_size = 4096 +local str_buf +local size_ptr +local FREE_LIST_REF = 0 + + +if subsystem == 'http' then + local ngx_lua_v = ngx.config.ngx_lua_version + if not ngx.config + or not ngx.config.ngx_lua_version + or (ngx_lua_v ~= 10016 and ngx_lua_v ~= 10017) + then + error("ngx_http_lua_module 0.10.16 or 0.10.17 required") + end + +elseif subsystem == 'stream' then + if not ngx.config + or not ngx.config.ngx_lua_version + or ngx.config.ngx_lua_version ~= 8 + then + error("ngx_stream_lua_module 0.0.8 required") + end + +else + error("ngx_http_lua_module 0.10.16 or " + .. "ngx_stream_lua_module 0.0.8 required") +end + + +if string.find(jit.version, " 2.0", 1, true) then + ngx.log(ngx.ALERT, "use of lua-resty-core with LuaJIT 2.0 is ", + "not recommended; use LuaJIT 2.1+ instead") +end + + +local ok, new_tab = pcall(require, "table.new") +if not ok then + new_tab = function (narr, nrec) return {} end +end + + +local clear_tab +ok, clear_tab = pcall(require, "table.clear") +if not ok then + local pairs = pairs + clear_tab = function (tab) + for k, _ in pairs(tab) do + tab[k] = nil + end + end +end + + +-- XXX for now LuaJIT 2.1 cannot compile require() +-- so we make the fast code path Lua only in our own +-- wrapper so that most of the require() calls in hot +-- Lua code paths can be JIT compiled. +do + local orig_require = require + local pkg_loaded = package.loaded + local function my_require(name) + local mod = pkg_loaded[name] + if mod then + return mod + end + return orig_require(name) + end + getfenv(0).require = my_require +end + + +if not pcall(ffi.typeof, "ngx_str_t") then + ffi.cdef[[ + typedef struct { + size_t len; + const unsigned char *data; + } ngx_str_t; + ]] +end + + +if subsystem == 'http' then + if not pcall(ffi.typeof, "ngx_http_request_t") then + ffi.cdef[[ + typedef struct ngx_http_request_s ngx_http_request_t; + ]] + end + + if not pcall(ffi.typeof, "ngx_http_lua_ffi_str_t") then + ffi.cdef[[ + typedef struct { + int len; + const unsigned char *data; + } ngx_http_lua_ffi_str_t; + ]] + end + +elseif subsystem == 'stream' then + if not pcall(ffi.typeof, "ngx_stream_lua_request_t") then + ffi.cdef[[ + typedef struct ngx_stream_lua_request_s ngx_stream_lua_request_t; + ]] + end + + if not pcall(ffi.typeof, "ngx_stream_lua_ffi_str_t") then + ffi.cdef[[ + typedef struct { + int len; + const unsigned char *data; + } ngx_stream_lua_ffi_str_t; + ]] + end + +else + error("unknown subsystem: " .. subsystem) +end + + +local c_buf_type = ffi.typeof("char[?]") + + +local _M = new_tab(0, 18) + + +_M.version = "0.1.19" +_M.new_tab = new_tab +_M.clear_tab = clear_tab + + +local errmsg + + +function _M.get_errmsg_ptr() + if not errmsg then + errmsg = ffi_new("char *[1]") + end + return errmsg +end + + +if not ngx then + error("no existing ngx. table found") +end + + +function _M.set_string_buf_size(size) + if size <= 0 then + return + end + if str_buf then + str_buf = nil + end + str_buf_size = ceil(size) +end + + +function _M.get_string_buf_size() + return str_buf_size +end + + +function _M.get_size_ptr() + if not size_ptr then + size_ptr = ffi_new("size_t[1]") + end + + return size_ptr +end + + +function _M.get_string_buf(size, must_alloc) + -- ngx.log(ngx.ERR, "str buf size: ", str_buf_size) + if size > str_buf_size or must_alloc then + return ffi_new(c_buf_type, size) + end + + if not str_buf then + str_buf = ffi_new(c_buf_type, str_buf_size) + end + + return str_buf +end + + +function _M.ref_in_table(tb, key) + if key == nil then + return -1 + end + local ref = tb[FREE_LIST_REF] + if ref and ref ~= 0 then + tb[FREE_LIST_REF] = tb[ref] + + else + ref = #tb + 1 + end + tb[ref] = key + + -- print("ref key_id returned ", ref) + return ref +end + + +function _M.allows_subsystem(...) + local total = select("#", ...) + + for i = 1, total do + if select(i, ...) == subsystem then + return + end + end + + error("unsupported subsystem: " .. subsystem, 2) +end + + +_M.FFI_OK = 0 +_M.FFI_NO_REQ_CTX = -100 +_M.FFI_BAD_CONTEXT = -101 +_M.FFI_ERROR = -1 +_M.FFI_AGAIN = -2 +_M.FFI_BUSY = -3 +_M.FFI_DONE = -4 +_M.FFI_DECLINED = -5 + + +do + local exdata + + ok, exdata = pcall(require, "thread.exdata") + if ok and exdata then + function _M.get_request() + local r = exdata() + if r ~= nil then + return r + end + end + + else + local getfenv = getfenv + + function _M.get_request() + return getfenv(0).__ngx_req + end + end +end + + +return _M diff --git a/resty/core/base64.lua b/resty/core/base64.lua new file mode 100644 index 0000000..8a0e463 --- /dev/null +++ b/resty/core/base64.lua @@ -0,0 +1,115 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_string = ffi.string +local ngx = ngx +local type = type +local error = error +local floor = math.floor +local tostring = tostring +local get_string_buf = base.get_string_buf +local get_size_ptr = base.get_size_ptr +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_encode_base64 +local ngx_lua_ffi_decode_base64 + + +if subsystem == "http" then + ffi.cdef[[ + size_t ngx_http_lua_ffi_encode_base64(const unsigned char *src, + size_t len, unsigned char *dst, + int no_padding); + + int ngx_http_lua_ffi_decode_base64(const unsigned char *src, + size_t len, unsigned char *dst, + size_t *dlen); + ]] + + ngx_lua_ffi_encode_base64 = C.ngx_http_lua_ffi_encode_base64 + ngx_lua_ffi_decode_base64 = C.ngx_http_lua_ffi_decode_base64 + +elseif subsystem == "stream" then + ffi.cdef[[ + size_t ngx_stream_lua_ffi_encode_base64(const unsigned char *src, + size_t len, unsigned char *dst, + int no_padding); + + int ngx_stream_lua_ffi_decode_base64(const unsigned char *src, + size_t len, unsigned char *dst, + size_t *dlen); + ]] + + ngx_lua_ffi_encode_base64 = C.ngx_stream_lua_ffi_encode_base64 + ngx_lua_ffi_decode_base64 = C.ngx_stream_lua_ffi_decode_base64 +end + + +local function base64_encoded_length(len, no_padding) + return no_padding and floor((len * 8 + 5) / 6) or + floor((len + 2) / 3) * 4 +end + + +ngx.encode_base64 = function (s, no_padding) + if type(s) ~= 'string' then + if not s then + s = '' + else + s = tostring(s) + end + end + + local slen = #s + local no_padding_bool = false; + local no_padding_int = 0; + + if no_padding then + if no_padding ~= true then + local typ = type(no_padding) + error("bad no_padding: boolean expected, got " .. typ, 2) + end + + no_padding_bool = true + no_padding_int = 1; + end + + local dlen = base64_encoded_length(slen, no_padding_bool) + local dst = get_string_buf(dlen) + local r_dlen = ngx_lua_ffi_encode_base64(s, slen, dst, no_padding_int) + -- if dlen ~= r_dlen then error("discrepancy in len") end + return ffi_string(dst, r_dlen) +end + + +local function base64_decoded_length(len) + return floor((len + 3) / 4) * 3 +end + + +ngx.decode_base64 = function (s) + if type(s) ~= 'string' then + error("string argument only", 2) + end + local slen = #s + local dlen = base64_decoded_length(slen) + -- print("dlen: ", tonumber(dlen)) + local dst = get_string_buf(dlen) + local pdlen = get_size_ptr() + local ok = ngx_lua_ffi_decode_base64(s, slen, dst, pdlen) + if ok == 0 then + return nil + end + return ffi_string(dst, pdlen[0]) +end + + +return { + version = base.version +} diff --git a/resty/core/ctx.lua b/resty/core/ctx.lua new file mode 100644 index 0000000..0683aaa --- /dev/null +++ b/resty/core/ctx.lua @@ -0,0 +1,101 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local debug = require "debug" +local base = require "resty.core.base" +local misc = require "resty.core.misc" + + +local C = ffi.C +local register_getter = misc.register_ngx_magic_key_getter +local register_setter = misc.register_ngx_magic_key_setter +local registry = debug.getregistry() +local new_tab = base.new_tab +local ref_in_table = base.ref_in_table +local get_request = base.get_request +local FFI_NO_REQ_CTX = base.FFI_NO_REQ_CTX +local FFI_OK = base.FFI_OK +local error = error +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_get_ctx_ref +local ngx_lua_ffi_set_ctx_ref + + +if subsystem == "http" then + ffi.cdef[[ + int ngx_http_lua_ffi_get_ctx_ref(ngx_http_request_t *r); + int ngx_http_lua_ffi_set_ctx_ref(ngx_http_request_t *r, int ref); + ]] + + ngx_lua_ffi_get_ctx_ref = C.ngx_http_lua_ffi_get_ctx_ref + ngx_lua_ffi_set_ctx_ref = C.ngx_http_lua_ffi_set_ctx_ref + +elseif subsystem == "stream" then + ffi.cdef[[ + int ngx_stream_lua_ffi_get_ctx_ref(ngx_stream_lua_request_t *r); + int ngx_stream_lua_ffi_set_ctx_ref(ngx_stream_lua_request_t *r, int ref); + ]] + + ngx_lua_ffi_get_ctx_ref = C.ngx_stream_lua_ffi_get_ctx_ref + ngx_lua_ffi_set_ctx_ref = C.ngx_stream_lua_ffi_set_ctx_ref +end + + +local _M = { + _VERSION = base.version +} + + +local function get_ctx_table() + local r = get_request() + + if not r then + error("no request found") + end + + local ctx_ref = ngx_lua_ffi_get_ctx_ref(r) + if ctx_ref == FFI_NO_REQ_CTX then + error("no request ctx found") + end + + local ctxs = registry.ngx_lua_ctx_tables + if ctx_ref < 0 then + local ctx = new_tab(0, 4) + ctx_ref = ref_in_table(ctxs, ctx) + if ngx_lua_ffi_set_ctx_ref(r, ctx_ref) ~= FFI_OK then + return nil + end + return ctx + end + return ctxs[ctx_ref] +end +register_getter("ctx", get_ctx_table) + + +local function set_ctx_table(ctx) + local r = get_request() + + if not r then + error("no request found") + end + + local ctx_ref = ngx_lua_ffi_get_ctx_ref(r) + if ctx_ref == FFI_NO_REQ_CTX then + error("no request ctx found") + end + + local ctxs = registry.ngx_lua_ctx_tables + if ctx_ref < 0 then + ctx_ref = ref_in_table(ctxs, ctx) + ngx_lua_ffi_set_ctx_ref(r, ctx_ref) + return + end + ctxs[ctx_ref] = ctx +end +register_setter("ctx", set_ctx_table) + + +return _M diff --git a/resty/core/exit.lua b/resty/core/exit.lua new file mode 100644 index 0000000..30a7b61 --- /dev/null +++ b/resty/core/exit.lua @@ -0,0 +1,66 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_string = ffi.string +local ngx = ngx +local error = error +local get_string_buf = base.get_string_buf +local get_size_ptr = base.get_size_ptr +local get_request = base.get_request +local co_yield = coroutine._yield +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_exit + + +if subsystem == "http" then + ffi.cdef[[ + int ngx_http_lua_ffi_exit(ngx_http_request_t *r, int status, + unsigned char *err, size_t *errlen); + ]] + + ngx_lua_ffi_exit = C.ngx_http_lua_ffi_exit + +elseif subsystem == "stream" then + ffi.cdef[[ + int ngx_stream_lua_ffi_exit(ngx_stream_lua_request_t *r, int status, + unsigned char *err, size_t *errlen); + ]] + + ngx_lua_ffi_exit = C.ngx_stream_lua_ffi_exit +end + + +local ERR_BUF_SIZE = 128 +local FFI_DONE = base.FFI_DONE + + +ngx.exit = function (rc) + local err = get_string_buf(ERR_BUF_SIZE) + local errlen = get_size_ptr() + local r = get_request() + if r == nil then + error("no request found") + end + errlen[0] = ERR_BUF_SIZE + rc = ngx_lua_ffi_exit(r, rc, err, errlen) + if rc == 0 then + -- print("yielding...") + return co_yield() + end + if rc == FFI_DONE then + return + end + error(ffi_string(err, errlen[0]), 2) +end + + +return { + version = base.version +} diff --git a/resty/core/hash.lua b/resty/core/hash.lua new file mode 100644 index 0000000..062f3ff --- /dev/null +++ b/resty/core/hash.lua @@ -0,0 +1,154 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_new = ffi.new +local ffi_string = ffi.string +local ngx = ngx +local type = type +local error = error +local tostring = tostring +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_md5 +local ngx_lua_ffi_md5_bin +local ngx_lua_ffi_sha1_bin +local ngx_lua_ffi_crc32_long +local ngx_lua_ffi_crc32_short + + +if subsystem == "http" then + ffi.cdef[[ + void ngx_http_lua_ffi_md5_bin(const unsigned char *src, size_t len, + unsigned char *dst); + + void ngx_http_lua_ffi_md5(const unsigned char *src, size_t len, + unsigned char *dst); + + int ngx_http_lua_ffi_sha1_bin(const unsigned char *src, size_t len, + unsigned char *dst); + + unsigned int ngx_http_lua_ffi_crc32_long(const unsigned char *src, + size_t len); + + unsigned int ngx_http_lua_ffi_crc32_short(const unsigned char *src, + size_t len); + ]] + + ngx_lua_ffi_md5 = C.ngx_http_lua_ffi_md5 + ngx_lua_ffi_md5_bin = C.ngx_http_lua_ffi_md5_bin + ngx_lua_ffi_sha1_bin = C.ngx_http_lua_ffi_sha1_bin + ngx_lua_ffi_crc32_short = C.ngx_http_lua_ffi_crc32_short + ngx_lua_ffi_crc32_long = C.ngx_http_lua_ffi_crc32_long + +elseif subsystem == "stream" then + ffi.cdef[[ + void ngx_stream_lua_ffi_md5_bin(const unsigned char *src, size_t len, + unsigned char *dst); + + void ngx_stream_lua_ffi_md5(const unsigned char *src, size_t len, + unsigned char *dst); + + int ngx_stream_lua_ffi_sha1_bin(const unsigned char *src, size_t len, + unsigned char *dst); + + unsigned int ngx_stream_lua_ffi_crc32_long(const unsigned char *src, + size_t len); + + unsigned int ngx_stream_lua_ffi_crc32_short(const unsigned char *src, + size_t len); + ]] + + ngx_lua_ffi_md5 = C.ngx_stream_lua_ffi_md5 + ngx_lua_ffi_md5_bin = C.ngx_stream_lua_ffi_md5_bin + ngx_lua_ffi_sha1_bin = C.ngx_stream_lua_ffi_sha1_bin + ngx_lua_ffi_crc32_short = C.ngx_stream_lua_ffi_crc32_short + ngx_lua_ffi_crc32_long = C.ngx_stream_lua_ffi_crc32_long +end + + +local MD5_DIGEST_LEN = 16 +local md5_buf = ffi_new("unsigned char[?]", MD5_DIGEST_LEN) + +ngx.md5_bin = function (s) + if type(s) ~= 'string' then + if not s then + s = '' + else + s = tostring(s) + end + end + ngx_lua_ffi_md5_bin(s, #s, md5_buf) + return ffi_string(md5_buf, MD5_DIGEST_LEN) +end + + +local MD5_HEX_DIGEST_LEN = MD5_DIGEST_LEN * 2 +local md5_hex_buf = ffi_new("unsigned char[?]", MD5_HEX_DIGEST_LEN) + +ngx.md5 = function (s) + if type(s) ~= 'string' then + if not s then + s = '' + else + s = tostring(s) + end + end + ngx_lua_ffi_md5(s, #s, md5_hex_buf) + return ffi_string(md5_hex_buf, MD5_HEX_DIGEST_LEN) +end + + +local SHA_DIGEST_LEN = 20 +local sha_buf = ffi_new("unsigned char[?]", SHA_DIGEST_LEN) + +ngx.sha1_bin = function (s) + if type(s) ~= 'string' then + if not s then + s = '' + else + s = tostring(s) + end + end + local ok = ngx_lua_ffi_sha1_bin(s, #s, sha_buf) + if ok == 0 then + error("SHA-1 support missing in Nginx") + end + return ffi_string(sha_buf, SHA_DIGEST_LEN) +end + + +ngx.crc32_short = function (s) + if type(s) ~= "string" then + if not s then + s = "" + else + s = tostring(s) + end + end + + return ngx_lua_ffi_crc32_short(s, #s) +end + + +ngx.crc32_long = function (s) + if type(s) ~= "string" then + if not s then + s = "" + else + s = tostring(s) + end + end + + return ngx_lua_ffi_crc32_long(s, #s) +end + + +return { + version = base.version +} diff --git a/resty/core/misc.lua b/resty/core/misc.lua new file mode 100644 index 0000000..ff7954a --- /dev/null +++ b/resty/core/misc.lua @@ -0,0 +1,240 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local base = require "resty.core.base" +local ffi = require "ffi" +local os = require "os" + + +local C = ffi.C +local ffi_new = ffi.new +local ffi_str = ffi.string +local ngx = ngx +local type = type +local error = error +local rawget = rawget +local rawset = rawset +local tonumber = tonumber +local setmetatable = setmetatable +local FFI_OK = base.FFI_OK +local FFI_NO_REQ_CTX = base.FFI_NO_REQ_CTX +local FFI_BAD_CONTEXT = base.FFI_BAD_CONTEXT +local new_tab = base.new_tab +local get_request = base.get_request +local get_size_ptr = base.get_size_ptr +local get_string_buf = base.get_string_buf +local get_string_buf_size = base.get_string_buf_size +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_get_resp_status +local ngx_lua_ffi_get_conf_env +local ngx_magic_key_getters +local ngx_magic_key_setters + + +local _M = new_tab(0, 3) +local ngx_mt = new_tab(0, 2) + + +if subsystem == "http" then + ngx_magic_key_getters = new_tab(0, 4) + ngx_magic_key_setters = new_tab(0, 2) + +elseif subsystem == "stream" then + ngx_magic_key_getters = new_tab(0, 2) + ngx_magic_key_setters = new_tab(0, 1) +end + + +local function register_getter(key, func) + ngx_magic_key_getters[key] = func +end +_M.register_ngx_magic_key_getter = register_getter + + +local function register_setter(key, func) + ngx_magic_key_setters[key] = func +end +_M.register_ngx_magic_key_setter = register_setter + + +ngx_mt.__index = function (tb, key) + local f = ngx_magic_key_getters[key] + if f then + return f() + end + return rawget(tb, key) +end + + +ngx_mt.__newindex = function (tb, key, ctx) + local f = ngx_magic_key_setters[key] + if f then + return f(ctx) + end + return rawset(tb, key, ctx) +end + + +setmetatable(ngx, ngx_mt) + + +if subsystem == "http" then + ffi.cdef[[ + int ngx_http_lua_ffi_get_resp_status(ngx_http_request_t *r); + int ngx_http_lua_ffi_set_resp_status(ngx_http_request_t *r, int r); + int ngx_http_lua_ffi_is_subrequest(ngx_http_request_t *r); + int ngx_http_lua_ffi_headers_sent(ngx_http_request_t *r); + int ngx_http_lua_ffi_get_conf_env(const unsigned char *name, + unsigned char **env_buf, + size_t *name_len); + ]] + + + ngx_lua_ffi_get_resp_status = C.ngx_http_lua_ffi_get_resp_status + ngx_lua_ffi_get_conf_env = C.ngx_http_lua_ffi_get_conf_env + + + -- ngx.status + + + local function set_status(status) + local r = get_request() + + if not r then + error("no request found") + end + + if type(status) ~= 'number' then + status = tonumber(status) + end + + local rc = C.ngx_http_lua_ffi_set_resp_status(r, status) + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + return + end + register_setter("status", set_status) + + + -- ngx.is_subrequest + + + local function is_subreq() + local r = get_request() + + if not r then + error("no request found") + end + + local rc = C.ngx_http_lua_ffi_is_subrequest(r) + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + return rc == 1 + end + register_getter("is_subrequest", is_subreq) + + + -- ngx.headers_sent + + + local function headers_sent() + local r = get_request() + + if not r then + error("no request found") + end + + local rc = C.ngx_http_lua_ffi_headers_sent(r) + + if rc == FFI_NO_REQ_CTX then + error("no request ctx found") + end + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + return rc == 1 + end + register_getter("headers_sent", headers_sent) + +elseif subsystem == "stream" then + ffi.cdef[[ + int ngx_stream_lua_ffi_get_resp_status(ngx_stream_lua_request_t *r); + int ngx_stream_lua_ffi_get_conf_env(const unsigned char *name, + unsigned char **env_buf, + size_t *name_len); + ]] + + ngx_lua_ffi_get_resp_status = C.ngx_stream_lua_ffi_get_resp_status + ngx_lua_ffi_get_conf_env = C.ngx_stream_lua_ffi_get_conf_env +end + + +-- ngx.status + + +local function get_status() + local r = get_request() + + if not r then + error("no request found") + end + + local rc = ngx_lua_ffi_get_resp_status(r) + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + return rc +end +register_getter("status", get_status) + + +do + local _getenv = os.getenv + local env_ptr = ffi_new("unsigned char *[1]") + + os.getenv = function (name) + local r = get_request() + if r then + -- past init_by_lua* phase now + os.getenv = _getenv + env_ptr = nil + return os.getenv(name) + end + + local size = get_string_buf_size() + env_ptr[0] = get_string_buf(size) + local name_len_ptr = get_size_ptr() + + local rc = ngx_lua_ffi_get_conf_env(name, env_ptr, name_len_ptr) + if rc == FFI_OK then + return ffi_str(env_ptr[0] + name_len_ptr[0] + 1) + end + + -- FFI_DECLINED + + local value = _getenv(name) + if value ~= nil then + return value + end + + return nil + end +end + + +_M._VERSION = base.version + + +return _M diff --git a/resty/core/ndk.lua b/resty/core/ndk.lua new file mode 100644 index 0000000..6547fe5 --- /dev/null +++ b/resty/core/ndk.lua @@ -0,0 +1,92 @@ +-- Copyright (C) by OpenResty Inc. + + +local ffi = require 'ffi' +local base = require "resty.core.base" +base.allows_subsystem('http') + + +local C = ffi.C +local ffi_cast = ffi.cast +local ffi_new = ffi.new +local ffi_str = ffi.string +local FFI_OK = base.FFI_OK +local new_tab = base.new_tab +local get_string_buf = base.get_string_buf +local get_request = base.get_request +local setmetatable = setmetatable +local type = type +local tostring = tostring +local error = error + + +local _M = { + version = base.version +} + + +ffi.cdef[[ +typedef void * ndk_set_var_value_pt; + +int ngx_http_lua_ffi_ndk_lookup_directive(const unsigned char *var_data, + size_t var_len, ndk_set_var_value_pt *func); +int ngx_http_lua_ffi_ndk_set_var_get(ngx_http_request_t *r, + ndk_set_var_value_pt func, const unsigned char *arg_data, size_t arg_len, + ngx_http_lua_ffi_str_t *value); +]] + + +local func_p = ffi_new("void*[1]") +local ffi_str_size = ffi.sizeof("ngx_http_lua_ffi_str_t") +local ffi_str_type = ffi.typeof("ngx_http_lua_ffi_str_t*") + + +local function ndk_set_var_get(self, var) + if type(var) ~= "string" then + var = tostring(var) + end + + if C.ngx_http_lua_ffi_ndk_lookup_directive(var, #var, func_p) ~= FFI_OK then + error('ndk.set_var: directive "' .. var + .. '" not found or does not use ndk_set_var_value', 2) + end + + local func = func_p[0] + + return function (arg) + local r = get_request() + if not r then + error("no request found") + end + + if type(arg) ~= "string" then + arg = tostring(arg) + end + + local buf = get_string_buf(ffi_str_size) + local value = ffi_cast(ffi_str_type, buf) + local rc = C.ngx_http_lua_ffi_ndk_set_var_get(r, func, arg, #arg, value) + if rc ~= FFI_OK then + error("calling directive " .. var .. " failed with code " .. rc, 2) + end + + return ffi_str(value.data, value.len) + end +end + + +local function ndk_set_var_set() + error("not allowed", 2) +end + + +if ndk then + local mt = new_tab(0, 2) + mt.__newindex = ndk_set_var_set + mt.__index = ndk_set_var_get + + ndk.set_var = setmetatable(new_tab(0, 0), mt) +end + + +return _M diff --git a/resty/core/phase.lua b/resty/core/phase.lua new file mode 100644 index 0000000..d30a534 --- /dev/null +++ b/resty/core/phase.lua @@ -0,0 +1,58 @@ +local ffi = require 'ffi' +local base = require "resty.core.base" + +local C = ffi.C +local FFI_ERROR = base.FFI_ERROR +local get_request = base.get_request +local error = error +local tostring = tostring + + +ffi.cdef[[ +int ngx_http_lua_ffi_get_phase(ngx_http_request_t *r, char **err) +]] + + +local errmsg = base.get_errmsg_ptr() +local context_names = { + [0x0001] = "set", + [0x0002] = "rewrite", + [0x0004] = "access", + [0x0008] = "content", + [0x0010] = "log", + [0x0020] = "header_filter", + [0x0040] = "body_filter", + [0x0080] = "timer", + [0x0100] = "init_worker", + [0x0200] = "balancer", + [0x0400] = "ssl_cert", + [0x0800] = "ssl_session_store", + [0x1000] = "ssl_session_fetch", +} + + +function ngx.get_phase() + local r = get_request() + + -- if we have no request object, assume we are called from the "init" phase + if not r then + return "init" + end + + local context = C.ngx_http_lua_ffi_get_phase(r, errmsg) + if context == FFI_ERROR then -- NGX_ERROR + error(errmsg, 2) + end + + local phase = context_names[context] + if not phase then + error("unknown phase: " .. tostring(context)) + end + + return phase +end + + +return { + version = base.version +} diff --git a/resty/core/regex.lua b/resty/core/regex.lua new file mode 100644 index 0000000..6de764f --- /dev/null +++ b/resty/core/regex.lua @@ -0,0 +1,1200 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local base = require "resty.core.base" +local bit = require "bit" +local subsystem = ngx.config.subsystem +require "resty.core.time" -- for ngx.now used by resty.lrucache + +if subsystem == 'http' then + require "resty.core.phase" -- for ngx.get_phase +end + +local lrucache = require "resty.lrucache" + +local lrucache_get = lrucache.get +local lrucache_set = lrucache.set +local ffi_string = ffi.string +local ffi_gc = ffi.gc +local ffi_copy = ffi.copy +local ffi_cast = ffi.cast +local C = ffi.C +local bor = bit.bor +local band = bit.band +local lshift = bit.lshift +local sub = string.sub +local fmt = string.format +local byte = string.byte +local ngx = ngx +local type = type +local tostring = tostring +local error = error +local setmetatable = setmetatable +local tonumber = tonumber +local get_string_buf = base.get_string_buf +local get_string_buf_size = base.get_string_buf_size +local new_tab = base.new_tab +local ngx_phase = ngx.get_phase +local ngx_log = ngx.log +local ngx_NOTICE = ngx.NOTICE + + +local _M = { + version = base.version +} + + +ngx.re = new_tab(0, 5) + + +ffi.cdef[[ + const char *pcre_version(void); +]] + + +local pcre_ver + +if not pcall(function() pcre_ver = ffi_string(C.pcre_version()) end) then + setmetatable(ngx.re, { + __index = function(_, key) + error("no support for 'ngx.re." .. key .. "': OpenResty was " .. + "compiled without PCRE support", 2) + end + }) + + _M.no_pcre = true + + return _M +end + + +local MAX_ERR_MSG_LEN = 128 + + +local FLAG_COMPILE_ONCE = 0x01 +local FLAG_DFA = 0x02 +local FLAG_JIT = 0x04 +local FLAG_DUPNAMES = 0x08 +local FLAG_NO_UTF8_CHECK = 0x10 + + +local PCRE_CASELESS = 0x0000001 +local PCRE_MULTILINE = 0x0000002 +local PCRE_DOTALL = 0x0000004 +local PCRE_EXTENDED = 0x0000008 +local PCRE_ANCHORED = 0x0000010 +local PCRE_UTF8 = 0x0000800 +local PCRE_DUPNAMES = 0x0080000 +local PCRE_JAVASCRIPT_COMPAT = 0x2000000 + + +local PCRE_ERROR_NOMATCH = -1 + + +local regex_match_cache +local regex_sub_func_cache = new_tab(0, 4) +local regex_sub_str_cache = new_tab(0, 4) +local max_regex_cache_size +local regex_cache_size = 0 +local script_engine +local ngx_lua_ffi_max_regex_cache_size +local ngx_lua_ffi_destroy_regex +local ngx_lua_ffi_compile_regex +local ngx_lua_ffi_exec_regex +local ngx_lua_ffi_create_script_engine +local ngx_lua_ffi_destroy_script_engine +local ngx_lua_ffi_init_script_engine +local ngx_lua_ffi_compile_replace_template +local ngx_lua_ffi_script_eval_len +local ngx_lua_ffi_script_eval_data + +-- PCRE 8.43 on macOS introduced the MAP_JIT option when creating the memory +-- region used to store JIT compiled code, which does not survive across +-- `fork()`, causing further usage of PCRE JIT compiler to segfault in worker +-- processes. +-- +-- This flag prevents any regex used in the init phase to be JIT compiled or +-- cached when running under macOS, even if the user requests so. Caching is +-- thus disabled to prevent further calls of same regex in worker to have poor +-- performance. +-- +-- TODO: improve this workaround when PCRE allows for unspecifying the MAP_JIT +-- option. +local no_jit_in_init + +if jit.os == "OSX" then + local maj, min = string.match(pcre_ver, "^(%d+)%.(%d+)") + if maj and min then + local pcre_ver_num = tonumber(maj .. min) + + if pcre_ver_num >= 843 then + no_jit_in_init = true + end + + else + -- assume this version is faulty as well + no_jit_in_init = true + end +end + + +if subsystem == 'http' then + ffi.cdef[[ + + typedef struct { + ngx_str_t value; + void *lengths; + void *values; + } ngx_http_lua_complex_value_t; + + typedef struct { + void *pool; + unsigned char *name_table; + int name_count; + int name_entry_size; + + int ncaptures; + int *captures; + + void *regex; + void *regex_sd; + + ngx_http_lua_complex_value_t *replace; + + const char *pattern; + } ngx_http_lua_regex_t; + + ngx_http_lua_regex_t * + ngx_http_lua_ffi_compile_regex(const unsigned char *pat, + size_t pat_len, int flags, + int pcre_opts, unsigned char *errstr, + size_t errstr_size); + + int ngx_http_lua_ffi_exec_regex(ngx_http_lua_regex_t *re, int flags, + const unsigned char *s, size_t len, int pos); + + void ngx_http_lua_ffi_destroy_regex(ngx_http_lua_regex_t *re); + + int ngx_http_lua_ffi_compile_replace_template(ngx_http_lua_regex_t *re, + const unsigned char + *replace_data, + size_t replace_len); + + struct ngx_http_lua_script_engine_s; + typedef struct ngx_http_lua_script_engine_s *ngx_http_lua_script_engine_t; + + ngx_http_lua_script_engine_t *ngx_http_lua_ffi_create_script_engine(void); + + void ngx_http_lua_ffi_init_script_engine(ngx_http_lua_script_engine_t *e, + const unsigned char *subj, + ngx_http_lua_regex_t *compiled, + int count); + + void ngx_http_lua_ffi_destroy_script_engine( + ngx_http_lua_script_engine_t *e); + + size_t ngx_http_lua_ffi_script_eval_len(ngx_http_lua_script_engine_t *e, + ngx_http_lua_complex_value_t *cv); + + size_t ngx_http_lua_ffi_script_eval_data(ngx_http_lua_script_engine_t *e, + ngx_http_lua_complex_value_t *cv, + unsigned char *dst); + + uint32_t ngx_http_lua_ffi_max_regex_cache_size(void); + ]] + + ngx_lua_ffi_max_regex_cache_size = C.ngx_http_lua_ffi_max_regex_cache_size + ngx_lua_ffi_destroy_regex = C.ngx_http_lua_ffi_destroy_regex + ngx_lua_ffi_compile_regex = C.ngx_http_lua_ffi_compile_regex + ngx_lua_ffi_exec_regex = C.ngx_http_lua_ffi_exec_regex + ngx_lua_ffi_create_script_engine = C.ngx_http_lua_ffi_create_script_engine + ngx_lua_ffi_init_script_engine = C.ngx_http_lua_ffi_init_script_engine + ngx_lua_ffi_destroy_script_engine = C.ngx_http_lua_ffi_destroy_script_engine + ngx_lua_ffi_compile_replace_template = + C.ngx_http_lua_ffi_compile_replace_template + ngx_lua_ffi_script_eval_len = C.ngx_http_lua_ffi_script_eval_len + ngx_lua_ffi_script_eval_data = C.ngx_http_lua_ffi_script_eval_data + +elseif subsystem == 'stream' then + ffi.cdef[[ + + typedef struct { + ngx_str_t value; + void *lengths; + void *values; + } ngx_stream_lua_complex_value_t; + + typedef struct { + void *pool; + unsigned char *name_table; + int name_count; + int name_entry_size; + + int ncaptures; + int *captures; + + void *regex; + void *regex_sd; + + ngx_stream_lua_complex_value_t *replace; + + const char *pattern; + } ngx_stream_lua_regex_t; + + ngx_stream_lua_regex_t * + ngx_stream_lua_ffi_compile_regex(const unsigned char *pat, + size_t pat_len, int flags, + int pcre_opts, unsigned char *errstr, + size_t errstr_size); + + int ngx_stream_lua_ffi_exec_regex(ngx_stream_lua_regex_t *re, int flags, + const unsigned char *s, size_t len, int pos); + + void ngx_stream_lua_ffi_destroy_regex(ngx_stream_lua_regex_t *re); + + int ngx_stream_lua_ffi_compile_replace_template(ngx_stream_lua_regex_t *re, + const unsigned char + *replace_data, + size_t replace_len); + + struct ngx_stream_lua_script_engine_s; + typedef struct ngx_stream_lua_script_engine_s + *ngx_stream_lua_script_engine_t; + + ngx_stream_lua_script_engine_t * + ngx_stream_lua_ffi_create_script_engine(void); + + void ngx_stream_lua_ffi_init_script_engine( + ngx_stream_lua_script_engine_t *e, const unsigned char *subj, + ngx_stream_lua_regex_t *compiled, int count); + + void ngx_stream_lua_ffi_destroy_script_engine( + ngx_stream_lua_script_engine_t *e); + + size_t ngx_stream_lua_ffi_script_eval_len( + ngx_stream_lua_script_engine_t *e, ngx_stream_lua_complex_value_t *cv); + + size_t ngx_stream_lua_ffi_script_eval_data( + ngx_stream_lua_script_engine_t *e, ngx_stream_lua_complex_value_t *cv, + unsigned char *dst); + + uint32_t ngx_stream_lua_ffi_max_regex_cache_size(void); + ]] + + ngx_lua_ffi_max_regex_cache_size = C.ngx_stream_lua_ffi_max_regex_cache_size + ngx_lua_ffi_destroy_regex = C.ngx_stream_lua_ffi_destroy_regex + ngx_lua_ffi_compile_regex = C.ngx_stream_lua_ffi_compile_regex + ngx_lua_ffi_exec_regex = C.ngx_stream_lua_ffi_exec_regex + ngx_lua_ffi_create_script_engine = C.ngx_stream_lua_ffi_create_script_engine + ngx_lua_ffi_init_script_engine = C.ngx_stream_lua_ffi_init_script_engine + ngx_lua_ffi_destroy_script_engine = + C.ngx_stream_lua_ffi_destroy_script_engine + ngx_lua_ffi_compile_replace_template = + C.ngx_stream_lua_ffi_compile_replace_template + ngx_lua_ffi_script_eval_len = C.ngx_stream_lua_ffi_script_eval_len + ngx_lua_ffi_script_eval_data = C.ngx_stream_lua_ffi_script_eval_data +end + + +local c_str_type = ffi.typeof("const char *") + +local cached_re_opts = new_tab(0, 4) + +local buf_grow_ratio = 2 + + +function _M.set_buf_grow_ratio(ratio) + buf_grow_ratio = ratio +end + + +local function get_max_regex_cache_size() + if max_regex_cache_size then + return max_regex_cache_size + end + max_regex_cache_size = ngx_lua_ffi_max_regex_cache_size() + return max_regex_cache_size +end + + +local regex_cache_is_empty = true + + +function _M.is_regex_cache_empty() + return regex_cache_is_empty +end + + +local function lrucache_set_wrapper(...) + regex_cache_is_empty = false + lrucache_set(...) +end + + +local parse_regex_opts = function (opts) + local t = cached_re_opts[opts] + if t then + return t[1], t[2] + end + + local flags = 0 + local pcre_opts = 0 + local len = #opts + + for i = 1, len do + local opt = byte(opts, i) + if opt == byte("o") then + flags = bor(flags, FLAG_COMPILE_ONCE) + + elseif opt == byte("j") then + flags = bor(flags, FLAG_JIT) + + elseif opt == byte("i") then + pcre_opts = bor(pcre_opts, PCRE_CASELESS) + + elseif opt == byte("s") then + pcre_opts = bor(pcre_opts, PCRE_DOTALL) + + elseif opt == byte("m") then + pcre_opts = bor(pcre_opts, PCRE_MULTILINE) + + elseif opt == byte("u") then + pcre_opts = bor(pcre_opts, PCRE_UTF8) + + elseif opt == byte("U") then + pcre_opts = bor(pcre_opts, PCRE_UTF8) + flags = bor(flags, FLAG_NO_UTF8_CHECK) + + elseif opt == byte("x") then + pcre_opts = bor(pcre_opts, PCRE_EXTENDED) + + elseif opt == byte("d") then + flags = bor(flags, FLAG_DFA) + + elseif opt == byte("a") then + pcre_opts = bor(pcre_opts, PCRE_ANCHORED) + + elseif opt == byte("D") then + pcre_opts = bor(pcre_opts, PCRE_DUPNAMES) + flags = bor(flags, FLAG_DUPNAMES) + + elseif opt == byte("J") then + pcre_opts = bor(pcre_opts, PCRE_JAVASCRIPT_COMPAT) + + else + error(fmt('unknown flag "%s" (flags "%s")', sub(opts, i, i), opts), + 3) + end + end + + cached_re_opts[opts] = {flags, pcre_opts} + return flags, pcre_opts +end + + +if no_jit_in_init then + local parse_regex_opts_ = parse_regex_opts + + parse_regex_opts = function (opts) + if ngx_phase() ~= "init" then + -- past init_by_lua* phase now + parse_regex_opts = parse_regex_opts_ + return parse_regex_opts(opts) + end + + local t = cached_re_opts[opts] + if t then + return t[1], t[2] + end + + local flags = 0 + local pcre_opts = 0 + local len = #opts + + for i = 1, len do + local opt = byte(opts, i) + if opt == byte("o") then + ngx_log(ngx_NOTICE, "regex compilation cache disabled in init ", + "phase under macOS") + + elseif opt == byte("j") then + ngx_log(ngx_NOTICE, "regex compilation disabled in init ", + "phase under macOS") + + elseif opt == byte("i") then + pcre_opts = bor(pcre_opts, PCRE_CASELESS) + + elseif opt == byte("s") then + pcre_opts = bor(pcre_opts, PCRE_DOTALL) + + elseif opt == byte("m") then + pcre_opts = bor(pcre_opts, PCRE_MULTILINE) + + elseif opt == byte("u") then + pcre_opts = bor(pcre_opts, PCRE_UTF8) + + elseif opt == byte("U") then + pcre_opts = bor(pcre_opts, PCRE_UTF8) + flags = bor(flags, FLAG_NO_UTF8_CHECK) + + elseif opt == byte("x") then + pcre_opts = bor(pcre_opts, PCRE_EXTENDED) + + elseif opt == byte("d") then + flags = bor(flags, FLAG_DFA) + + elseif opt == byte("a") then + pcre_opts = bor(pcre_opts, PCRE_ANCHORED) + + elseif opt == byte("D") then + pcre_opts = bor(pcre_opts, PCRE_DUPNAMES) + flags = bor(flags, FLAG_DUPNAMES) + + elseif opt == byte("J") then + pcre_opts = bor(pcre_opts, PCRE_JAVASCRIPT_COMPAT) + + else + error(fmt('unknown flag "%s" (flags "%s")', sub(opts, i, i), + opts), 3) + end + end + + cached_re_opts[opts] = {flags, pcre_opts} + return flags, pcre_opts + end +end + + +local function collect_named_captures(compiled, flags, res) + local name_count = compiled.name_count + local name_table = compiled.name_table + local entry_size = compiled.name_entry_size + + local ind = 0 + local dup_names = (band(flags, FLAG_DUPNAMES) ~= 0) + for i = 1, name_count do + local n = bor(lshift(name_table[ind], 8), name_table[ind + 1]) + -- ngx.say("n = ", n) + local name = ffi_string(name_table + ind + 2) + local cap = res[n] + if dup_names then + -- unmatched captures (false) are not collected + if cap then + local old = res[name] + if old then + old[#old + 1] = cap + else + res[name] = {cap} + end + end + else + res[name] = cap + end + + ind = ind + entry_size + end +end + + +local function collect_captures(compiled, rc, subj, flags, res) + local cap = compiled.captures + local ncap = compiled.ncaptures + local name_count = compiled.name_count + + if not res then + res = new_tab(ncap, name_count) + end + + local i = 0 + local n = 0 + while i <= ncap do + if i > rc then + res[i] = false + else + local from = cap[n] + if from >= 0 then + local to = cap[n + 1] + res[i] = sub(subj, from + 1, to) + else + res[i] = false + end + end + i = i + 1 + n = n + 2 + end + + if name_count > 0 then + collect_named_captures(compiled, flags, res) + end + + return res +end + + +_M.collect_captures = collect_captures + + +local function destroy_compiled_regex(compiled) + ngx_lua_ffi_destroy_regex(ffi_gc(compiled, nil)) +end + + +_M.destroy_compiled_regex = destroy_compiled_regex + + +local function re_match_compile(regex, opts) + local flags = 0 + local pcre_opts = 0 + + if opts then + flags, pcre_opts = parse_regex_opts(opts) + else + opts = "" + end + + local compiled, key + local compile_once = (band(flags, FLAG_COMPILE_ONCE) == 1) + + -- FIXME: better put this in the outer scope when fixing the ngx.re API's + -- compatibility in the init_by_lua* context. + if not regex_match_cache then + local sz = get_max_regex_cache_size() + if sz <= 0 then + compile_once = false + else + regex_match_cache = lrucache.new(sz) + end + end + + if compile_once then + key = regex .. '\0' .. opts + compiled = lrucache_get(regex_match_cache, key) + end + + -- compile the regex + + if compiled == nil then + -- print("compiled regex not found, compiling regex...") + local errbuf = get_string_buf(MAX_ERR_MSG_LEN) + + compiled = ngx_lua_ffi_compile_regex(regex, #regex, flags, + pcre_opts, errbuf, + MAX_ERR_MSG_LEN) + + if compiled == nil then + return nil, ffi_string(errbuf) + end + + ffi_gc(compiled, ngx_lua_ffi_destroy_regex) + + -- print("ncaptures: ", compiled.ncaptures) + + if compile_once then + -- print("inserting compiled regex into cache") + lrucache_set_wrapper(regex_match_cache, key, compiled) + end + end + + return compiled, compile_once, flags +end + + +_M.re_match_compile = re_match_compile + + +local function re_match_helper(subj, regex, opts, ctx, want_caps, res, nth) + -- we need to cast this to strings to avoid exceptions when they are + -- something else. + subj = tostring(subj) + + local compiled, compile_once, flags = re_match_compile(regex, opts) + if compiled == nil then + -- compiled_once holds the error string + if not want_caps then + return nil, nil, compile_once + end + return nil, compile_once + end + + -- exec the compiled regex + + local rc + do + local pos + if ctx then + pos = ctx.pos + if not pos or pos <= 0 then + pos = 0 + else + pos = pos - 1 + end + + else + pos = 0 + end + + rc = ngx_lua_ffi_exec_regex(compiled, flags, subj, #subj, pos) + end + + if rc == PCRE_ERROR_NOMATCH then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil + end + + if rc < 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + if not want_caps then + return nil, nil, "pcre_exec() failed: " .. rc + end + return nil, "pcre_exec() failed: " .. rc + end + + if rc == 0 then + if band(flags, FLAG_DFA) == 0 then + if not want_caps then + return nil, nil, "capture size too small" + end + return nil, "capture size too small" + end + + rc = 1 + end + + -- print("cap 0: ", compiled.captures[0]) + -- print("cap 1: ", compiled.captures[1]) + + if ctx then + ctx.pos = compiled.captures[1] + 1 + end + + if not want_caps then + if not nth or nth < 0 then + nth = 0 + end + + if nth > compiled.ncaptures then + return nil, nil, "nth out of bound" + end + + if nth >= rc then + return nil, nil + end + + local from = compiled.captures[nth * 2] + 1 + local to = compiled.captures[nth * 2 + 1] + + if from < 0 or to < 0 then + return nil, nil + end + + return from, to + end + + res = collect_captures(compiled, rc, subj, flags, res) + + if not compile_once then + destroy_compiled_regex(compiled) + end + + return res +end + + +function ngx.re.match(subj, regex, opts, ctx, res) + return re_match_helper(subj, regex, opts, ctx, true, res) +end + + +function ngx.re.find(subj, regex, opts, ctx, nth) + return re_match_helper(subj, regex, opts, ctx, false, nil, nth) +end + + +do + local function destroy_re_gmatch_iterator(iterator) + if not iterator._compile_once then + destroy_compiled_regex(iterator._compiled) + end + iterator._compiled = nil + iterator._pos = nil + iterator._subj = nil + end + + + local function iterate_re_gmatch(self) + local compiled = self._compiled + local subj = self._subj + local subj_len = self._subj_len + local flags = self._flags + local pos = self._pos + + if not pos then + -- The iterator is exhausted. + return nil + end + + local rc = ngx_lua_ffi_exec_regex(compiled, flags, subj, subj_len, pos) + + if rc == PCRE_ERROR_NOMATCH then + destroy_re_gmatch_iterator(self) + return nil + end + + if rc < 0 then + destroy_re_gmatch_iterator(self) + return nil, "pcre_exec() failed: " .. rc + end + + if rc == 0 then + if band(flags, FLAG_DFA) == 0 then + destroy_re_gmatch_iterator(self) + return nil, "capture size too small" + end + + rc = 1 + end + + local cp_pos = tonumber(compiled.captures[1]) + if cp_pos == compiled.captures[0] then + cp_pos = cp_pos + 1 + if cp_pos > subj_len then + local res = collect_captures(compiled, rc, subj, flags) + destroy_re_gmatch_iterator(self) + return res + end + end + self._pos = cp_pos + return collect_captures(compiled, rc, subj, flags) + end + + + local re_gmatch_iterator_mt = { __call = iterate_re_gmatch } + + function ngx.re.gmatch(subj, regex, opts) + subj = tostring(subj) + + local compiled, compile_once, flags = re_match_compile(regex, opts) + if compiled == nil then + -- compiled_once holds the error string + return nil, compile_once + end + + local re_gmatch_iterator = { + _compiled = compiled, + _compile_once = compile_once, + _subj = subj, + _subj_len = #subj, + _flags = flags, + _pos = 0, + } + + return setmetatable(re_gmatch_iterator, re_gmatch_iterator_mt) + end +end -- do + + +local function new_script_engine(subj, compiled, count) + if not script_engine then + script_engine = ngx_lua_ffi_create_script_engine() + if script_engine == nil then + return nil + end + ffi_gc(script_engine, ngx_lua_ffi_destroy_script_engine) + end + + ngx_lua_ffi_init_script_engine(script_engine, subj, compiled, count) + return script_engine +end + + +local function check_buf_size(buf, buf_size, pos, len, new_len, must_alloc) + if new_len > buf_size then + buf_size = buf_size * buf_grow_ratio + if buf_size < new_len then + buf_size = new_len + end + local new_buf = get_string_buf(buf_size, must_alloc) + ffi_copy(new_buf, buf, len) + buf = new_buf + pos = buf + len + end + return buf, buf_size, pos, new_len +end + + +_M.check_buf_size = check_buf_size + + +local function re_sub_compile(regex, opts, replace, func) + local flags = 0 + local pcre_opts = 0 + + if opts then + flags, pcre_opts = parse_regex_opts(opts) + else + opts = "" + end + + local compiled + local compile_once = (band(flags, FLAG_COMPILE_ONCE) == 1) + if compile_once then + if func then + local subcache = regex_sub_func_cache[opts] + if subcache then + -- print("cache hit!") + compiled = subcache[regex] + end + + else + local subcache = regex_sub_str_cache[opts] + if subcache then + local subsubcache = subcache[regex] + if subsubcache then + -- print("cache hit!") + compiled = subsubcache[replace] + end + end + end + end + + -- compile the regex + + if compiled == nil then + -- print("compiled regex not found, compiling regex...") + local errbuf = get_string_buf(MAX_ERR_MSG_LEN) + + compiled = ngx_lua_ffi_compile_regex(regex, #regex, flags, pcre_opts, + errbuf, MAX_ERR_MSG_LEN) + + if compiled == nil then + return nil, ffi_string(errbuf) + end + + ffi_gc(compiled, ngx_lua_ffi_destroy_regex) + + if func == nil then + local rc = + ngx_lua_ffi_compile_replace_template(compiled, replace, + #replace) + if rc ~= 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil, "failed to compile the replacement template" + end + end + + -- print("ncaptures: ", compiled.ncaptures) + + if compile_once then + if regex_cache_size < get_max_regex_cache_size() then + -- print("inserting compiled regex into cache") + if func then + local subcache = regex_sub_func_cache[opts] + if not subcache then + regex_sub_func_cache[opts] = {[regex] = compiled} + + else + subcache[regex] = compiled + end + + else + local subcache = regex_sub_str_cache[opts] + if not subcache then + regex_sub_str_cache[opts] = + {[regex] = {[replace] = compiled}} + + else + local subsubcache = subcache[regex] + if not subsubcache then + subcache[regex] = {[replace] = compiled} + + else + subsubcache[replace] = compiled + end + end + end + + regex_cache_size = regex_cache_size + 1 + else + compile_once = false + end + end + end + + return compiled, compile_once, flags +end + + +_M.re_sub_compile = re_sub_compile + + +local function re_sub_func_helper(subj, regex, replace, opts, global) + local compiled, compile_once, flags = + re_sub_compile(regex, opts, nil, replace) + if not compiled then + -- error string is in compile_once + return nil, nil, compile_once + end + + -- exec the compiled regex + + subj = tostring(subj) + local subj_len = #subj + local count = 0 + local pos = 0 + local cp_pos = 0 + + local dst_buf_size = get_string_buf_size() + -- Note: we have to always allocate the string buffer because + -- the user might call whatever resty.core's API functions recursively + -- in the user callback function. + local dst_buf = get_string_buf(dst_buf_size, true) + local dst_pos = dst_buf + local dst_len = 0 + + while true do + local rc = ngx_lua_ffi_exec_regex(compiled, flags, subj, subj_len, pos) + if rc == PCRE_ERROR_NOMATCH then + break + end + + if rc < 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil, nil, "pcre_exec() failed: " .. rc + end + + if rc == 0 then + if band(flags, FLAG_DFA) == 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil, nil, "capture size too small" + end + + rc = 1 + end + + count = count + 1 + local prefix_len = compiled.captures[0] - cp_pos + + local res = collect_captures(compiled, rc, subj, flags) + + local piece = tostring(replace(res)) + local piece_len = #piece + + local new_dst_len = dst_len + prefix_len + piece_len + dst_buf, dst_buf_size, dst_pos, dst_len = + check_buf_size(dst_buf, dst_buf_size, dst_pos, dst_len, + new_dst_len, true) + + if prefix_len > 0 then + ffi_copy(dst_pos, ffi_cast(c_str_type, subj) + cp_pos, + prefix_len) + dst_pos = dst_pos + prefix_len + end + + if piece_len > 0 then + ffi_copy(dst_pos, piece, piece_len) + dst_pos = dst_pos + piece_len + end + + cp_pos = compiled.captures[1] + pos = cp_pos + if pos == compiled.captures[0] then + pos = pos + 1 + if pos > subj_len then + break + end + end + + if not global then + break + end + end + + if not compile_once then + destroy_compiled_regex(compiled) + end + + if count > 0 then + if pos < subj_len then + local suffix_len = subj_len - cp_pos + + local new_dst_len = dst_len + suffix_len + local _ + dst_buf, _, dst_pos, dst_len = + check_buf_size(dst_buf, dst_buf_size, dst_pos, dst_len, + new_dst_len, true) + + ffi_copy(dst_pos, ffi_cast(c_str_type, subj) + cp_pos, + suffix_len) + end + return ffi_string(dst_buf, dst_len), count + end + + return subj, 0 +end + + +local function re_sub_str_helper(subj, regex, replace, opts, global) + local compiled, compile_once, flags = + re_sub_compile(regex, opts, replace, nil) + if not compiled then + -- error string is in compile_once + return nil, nil, compile_once + end + + -- exec the compiled regex + + subj = tostring(subj) + local subj_len = #subj + local count = 0 + local pos = 0 + local cp_pos = 0 + + local dst_buf_size = get_string_buf_size() + local dst_buf = get_string_buf(dst_buf_size) + local dst_pos = dst_buf + local dst_len = 0 + + while true do + local rc = ngx_lua_ffi_exec_regex(compiled, flags, subj, subj_len, pos) + if rc == PCRE_ERROR_NOMATCH then + break + end + + if rc < 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil, nil, "pcre_exec() failed: " .. rc + end + + if rc == 0 then + if band(flags, FLAG_DFA) == 0 then + if not compile_once then + destroy_compiled_regex(compiled) + end + return nil, nil, "capture size too small" + end + + rc = 1 + end + + count = count + 1 + local prefix_len = compiled.captures[0] - cp_pos + + local cv = compiled.replace + if cv.lengths ~= nil then + local e = new_script_engine(subj, compiled, rc) + if e == nil then + return nil, nil, "failed to create script engine" + end + + local bit_len = ngx_lua_ffi_script_eval_len(e, cv) + local new_dst_len = dst_len + prefix_len + bit_len + dst_buf, dst_buf_size, dst_pos, dst_len = + check_buf_size(dst_buf, dst_buf_size, dst_pos, dst_len, + new_dst_len) + + if prefix_len > 0 then + ffi_copy(dst_pos, ffi_cast(c_str_type, subj) + cp_pos, + prefix_len) + dst_pos = dst_pos + prefix_len + end + + if bit_len > 0 then + ngx_lua_ffi_script_eval_data(e, cv, dst_pos) + dst_pos = dst_pos + bit_len + end + + else + local bit_len = cv.value.len + + dst_buf, dst_buf_size, dst_pos, dst_len = + check_buf_size(dst_buf, dst_buf_size, dst_pos, dst_len, + dst_len + prefix_len + bit_len) + + if prefix_len > 0 then + ffi_copy(dst_pos, ffi_cast(c_str_type, subj) + cp_pos, + prefix_len) + dst_pos = dst_pos + prefix_len + end + + if bit_len > 0 then + ffi_copy(dst_pos, cv.value.data, bit_len) + dst_pos = dst_pos + bit_len + end + end + + cp_pos = compiled.captures[1] + pos = cp_pos + if pos == compiled.captures[0] then + pos = pos + 1 + if pos > subj_len then + break + end + end + + if not global then + break + end + end + + if not compile_once then + destroy_compiled_regex(compiled) + end + + if count > 0 then + if pos < subj_len then + local suffix_len = subj_len - cp_pos + + local new_dst_len = dst_len + suffix_len + local _ + dst_buf, _, dst_pos, dst_len = + check_buf_size(dst_buf, dst_buf_size, dst_pos, dst_len, + new_dst_len) + + ffi_copy(dst_pos, ffi_cast(c_str_type, subj) + cp_pos, + suffix_len) + end + return ffi_string(dst_buf, dst_len), count + end + + return subj, 0 +end + + +local function re_sub_helper(subj, regex, replace, opts, global) + local repl_type = type(replace) + if repl_type == "function" then + return re_sub_func_helper(subj, regex, replace, opts, global) + end + + if repl_type ~= "string" then + replace = tostring(replace) + end + + return re_sub_str_helper(subj, regex, replace, opts, global) +end + + +function ngx.re.sub(subj, regex, replace, opts) + return re_sub_helper(subj, regex, replace, opts, false) +end + + +function ngx.re.gsub(subj, regex, replace, opts) + return re_sub_helper(subj, regex, replace, opts, true) +end + + +return _M diff --git a/resty/core/request.lua b/resty/core/request.lua new file mode 100644 index 0000000..f912bfa --- /dev/null +++ b/resty/core/request.lua @@ -0,0 +1,429 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local base = require "resty.core.base" +base.allows_subsystem("http") +local utils = require "resty.core.utils" + + +local FFI_BAD_CONTEXT = base.FFI_BAD_CONTEXT +local FFI_DECLINED = base.FFI_DECLINED +local FFI_OK = base.FFI_OK +local new_tab = base.new_tab +local C = ffi.C +local ffi_cast = ffi.cast +local ffi_new = ffi.new +local ffi_str = ffi.string +local get_string_buf = base.get_string_buf +local get_size_ptr = base.get_size_ptr +local setmetatable = setmetatable +local lower = string.lower +local rawget = rawget +local ngx = ngx +local get_request = base.get_request +local type = type +local error = error +local tostring = tostring +local tonumber = tonumber +local str_replace_char = utils.str_replace_char + + +local _M = { + version = base.version +} + + +local errmsg = base.get_errmsg_ptr() +local ffi_str_type = ffi.typeof("ngx_http_lua_ffi_str_t*") +local ffi_str_size = ffi.sizeof("ngx_http_lua_ffi_str_t") + + +ffi.cdef[[ + typedef struct { + ngx_http_lua_ffi_str_t key; + ngx_http_lua_ffi_str_t value; + } ngx_http_lua_ffi_table_elt_t; + + int ngx_http_lua_ffi_req_get_headers_count(ngx_http_request_t *r, + int max, int *truncated); + + int ngx_http_lua_ffi_req_get_headers(ngx_http_request_t *r, + ngx_http_lua_ffi_table_elt_t *out, int count, int raw); + + int ngx_http_lua_ffi_req_get_uri_args_count(ngx_http_request_t *r, + int max, int *truncated); + + size_t ngx_http_lua_ffi_req_get_querystring_len(ngx_http_request_t *r); + + int ngx_http_lua_ffi_req_get_uri_args(ngx_http_request_t *r, + unsigned char *buf, ngx_http_lua_ffi_table_elt_t *out, int count); + + double ngx_http_lua_ffi_req_start_time(ngx_http_request_t *r); + + int ngx_http_lua_ffi_req_get_method(ngx_http_request_t *r); + + int ngx_http_lua_ffi_req_get_method_name(ngx_http_request_t *r, + unsigned char **name, size_t *len); + + int ngx_http_lua_ffi_req_set_method(ngx_http_request_t *r, int method); + + int ngx_http_lua_ffi_req_set_header(ngx_http_request_t *r, + const unsigned char *key, size_t key_len, const unsigned char *value, + size_t value_len, ngx_http_lua_ffi_str_t *mvals, size_t mvals_len, + int override, char **errmsg); +]] + + +local table_elt_type = ffi.typeof("ngx_http_lua_ffi_table_elt_t*") +local table_elt_size = ffi.sizeof("ngx_http_lua_ffi_table_elt_t") +local truncated = ffi.new("int[1]") + +local req_headers_mt = { + __index = function (tb, key) + return rawget(tb, (str_replace_char(lower(key), '_', '-'))) + end +} + + +function ngx.req.get_headers(max_headers, raw) + local r = get_request() + if not r then + error("no request found") + end + + if not max_headers then + max_headers = -1 + end + + if not raw then + raw = 0 + else + raw = 1 + end + + local n = C.ngx_http_lua_ffi_req_get_headers_count(r, max_headers, + truncated) + if n == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + if n == 0 then + local headers = {} + if raw == 0 then + headers = setmetatable(headers, req_headers_mt) + end + + return headers + end + + local raw_buf = get_string_buf(n * table_elt_size) + local buf = ffi_cast(table_elt_type, raw_buf) + + local rc = C.ngx_http_lua_ffi_req_get_headers(r, buf, n, raw) + if rc == 0 then + local headers = new_tab(0, n) + for i = 0, n - 1 do + local h = buf[i] + + local key = h.key + key = ffi_str(key.data, key.len) + + local value = h.value + value = ffi_str(value.data, value.len) + + local existing = headers[key] + if existing then + if type(existing) == "table" then + existing[#existing + 1] = value + else + headers[key] = {existing, value} + end + + else + headers[key] = value + end + end + + if raw == 0 then + headers = setmetatable(headers, req_headers_mt) + end + + if truncated[0] ~= 0 then + return headers, "truncated" + end + + return headers + end + + return nil +end + + +function ngx.req.get_uri_args(max_args) + local r = get_request() + if not r then + error("no request found") + end + + if not max_args then + max_args = -1 + end + + local n = C.ngx_http_lua_ffi_req_get_uri_args_count(r, max_args, truncated) + if n == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + if n == 0 then + return {} + end + + local args_len = C.ngx_http_lua_ffi_req_get_querystring_len(r) + + local strbuf = get_string_buf(args_len + n * table_elt_size) + local kvbuf = ffi_cast(table_elt_type, strbuf + args_len) + + local nargs = C.ngx_http_lua_ffi_req_get_uri_args(r, strbuf, kvbuf, n) + + local args = new_tab(0, nargs) + for i = 0, nargs - 1 do + local arg = kvbuf[i] + + local key = arg.key + key = ffi_str(key.data, key.len) + + local value = arg.value + local len = value.len + if len == -1 then + value = true + else + value = ffi_str(value.data, len) + end + + local existing = args[key] + if existing then + if type(existing) == "table" then + existing[#existing + 1] = value + else + args[key] = {existing, value} + end + + else + args[key] = value + end + end + + if truncated[0] ~= 0 then + return args, "truncated" + end + + return args +end + + +function ngx.req.start_time() + local r = get_request() + if not r then + error("no request found") + end + + return tonumber(C.ngx_http_lua_ffi_req_start_time(r)) +end + + +do + local methods = { + [0x0002] = "GET", + [0x0004] = "HEAD", + [0x0008] = "POST", + [0x0010] = "PUT", + [0x0020] = "DELETE", + [0x0040] = "MKCOL", + [0x0080] = "COPY", + [0x0100] = "MOVE", + [0x0200] = "OPTIONS", + [0x0400] = "PROPFIND", + [0x0800] = "PROPPATCH", + [0x1000] = "LOCK", + [0x2000] = "UNLOCK", + [0x4000] = "PATCH", + [0x8000] = "TRACE", + } + + local namep = ffi_new("unsigned char *[1]") + + function ngx.req.get_method() + local r = get_request() + if not r then + error("no request found") + end + + do + local id = C.ngx_http_lua_ffi_req_get_method(r) + if id == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + local method = methods[id] + if method then + return method + end + end + + local sizep = get_size_ptr() + local rc = C.ngx_http_lua_ffi_req_get_method_name(r, namep, sizep) + if rc ~= 0 then + return nil + end + + return ffi_str(namep[0], sizep[0]) + end +end -- do + + +function ngx.req.set_method(method) + local r = get_request() + if not r then + error("no request found") + end + + if type(method) ~= "number" then + error("bad method number", 2) + end + + local rc = C.ngx_http_lua_ffi_req_set_method(r, method) + if rc == FFI_OK then + return + end + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + if rc == FFI_DECLINED then + error("unsupported HTTP method: " .. method, 2) + end + + error("unknown error: " .. rc) +end + + +do + local function set_req_header(name, value, override) + local r = get_request() + if not r then + error("no request found", 3) + end + + if name == nil then + error("bad 'name' argument: string expected, got nil", 3) + end + + if type(name) ~= "string" then + name = tostring(name) + end + + local rc + + if value == nil then + if not override then + error("bad 'value' argument: string or table expected, got nil", + 3) + end + + rc = C.ngx_http_lua_ffi_req_set_header(r, name, #name, nil, 0, nil, + 0, 1, errmsg) + + else + local sval, sval_len, mvals, mvals_len, buf + local value_type = type(value) + + if value_type == "table" then + mvals_len = #value + if mvals_len == 0 and not override then + error("bad 'value' argument: non-empty table expected", 3) + end + + buf = get_string_buf(ffi_str_size * mvals_len) + mvals = ffi_cast(ffi_str_type, buf) + + for i = 1, mvals_len do + local s = value[i] + if type(s) ~= "string" then + s = tostring(s) + value[i] = s + end + + local str = mvals[i - 1] + str.data = s + str.len = #s + end + + sval_len = 0 + + else + if value_type ~= "string" then + sval = tostring(value) + else + sval = value + end + + sval_len = #sval + mvals_len = 0 + end + + rc = C.ngx_http_lua_ffi_req_set_header(r, name, #name, sval, + sval_len, mvals, mvals_len, + override and 1 or 0, errmsg) + end + + if rc == FFI_OK or rc == FFI_DECLINED then + return + end + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 3) + end + + -- rc == FFI_ERROR + error(ffi_str(errmsg[0])) + end + + + _M.set_req_header = set_req_header + + + function ngx.req.set_header(name, value) + set_req_header(name, value, true) -- override + end +end -- do + + +function ngx.req.clear_header(name) + local r = get_request() + if not r then + error("no request found") + end + + if type(name) ~= "string" then + name = tostring(name) + end + + local rc = C.ngx_http_lua_ffi_req_set_header(r, name, #name, nil, 0, nil, 0, + 1, errmsg) + + if rc == FFI_OK or rc == FFI_DECLINED then + return + end + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + -- rc == FFI_ERROR + error(ffi_str(errmsg[0])) +end + + +return _M diff --git a/resty/core/response.lua b/resty/core/response.lua new file mode 100644 index 0000000..891a07e --- /dev/null +++ b/resty/core/response.lua @@ -0,0 +1,183 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_cast = ffi.cast +local ffi_str = ffi.string +local new_tab = base.new_tab +local FFI_BAD_CONTEXT = base.FFI_BAD_CONTEXT +local FFI_NO_REQ_CTX = base.FFI_NO_REQ_CTX +local FFI_DECLINED = base.FFI_DECLINED +local get_string_buf = base.get_string_buf +local setmetatable = setmetatable +local type = type +local tostring = tostring +local get_request = base.get_request +local error = error +local ngx = ngx + + +local _M = { + version = base.version +} + + +local MAX_HEADER_VALUES = 100 +local errmsg = base.get_errmsg_ptr() +local ffi_str_type = ffi.typeof("ngx_http_lua_ffi_str_t*") +local ffi_str_size = ffi.sizeof("ngx_http_lua_ffi_str_t") + + +ffi.cdef[[ + int ngx_http_lua_ffi_set_resp_header(ngx_http_request_t *r, + const char *key_data, size_t key_len, int is_nil, + const char *sval, size_t sval_len, ngx_http_lua_ffi_str_t *mvals, + size_t mvals_len, int override, char **errmsg); + + int ngx_http_lua_ffi_get_resp_header(ngx_http_request_t *r, + const unsigned char *key, size_t key_len, + unsigned char *key_buf, ngx_http_lua_ffi_str_t *values, + int max_nvalues, char **errmsg); +]] + + +local function set_resp_header(tb, key, value, no_override) + local r = get_request() + if not r then + error("no request found") + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local rc + if value == nil then + if no_override then + error("invalid header value", 3) + end + + rc = C.ngx_http_lua_ffi_set_resp_header(r, key, #key, true, nil, 0, nil, + 0, 1, errmsg) + else + local sval, sval_len, mvals, mvals_len, buf + + if type(value) == "table" then + mvals_len = #value + if mvals_len == 0 and no_override then + return + end + + buf = get_string_buf(ffi_str_size * mvals_len) + mvals = ffi_cast(ffi_str_type, buf) + for i = 1, mvals_len do + local s = value[i] + if type(s) ~= "string" then + s = tostring(s) + value[i] = s + end + local str = mvals[i - 1] + str.data = s + str.len = #s + end + + sval_len = 0 + + else + if type(value) ~= "string" then + sval = tostring(value) + else + sval = value + end + sval_len = #sval + + mvals_len = 0 + end + + local override_int = no_override and 0 or 1 + rc = C.ngx_http_lua_ffi_set_resp_header(r, key, #key, false, sval, + sval_len, mvals, mvals_len, + override_int, errmsg) + end + + if rc == 0 or rc == FFI_DECLINED then + return + end + + if rc == FFI_NO_REQ_CTX then + error("no request ctx found") + end + + if rc == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + -- rc == FFI_ERROR + error(ffi_str(errmsg[0]), 2) +end + + +_M.set_resp_header = set_resp_header + + +local function get_resp_header(tb, key) + local r = get_request() + if not r then + error("no request found") + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + + local key_buf = get_string_buf(key_len + ffi_str_size * MAX_HEADER_VALUES) + local values = ffi_cast(ffi_str_type, key_buf + key_len) + local n = C.ngx_http_lua_ffi_get_resp_header(r, key, key_len, key_buf, + values, MAX_HEADER_VALUES, + errmsg) + + -- print("retval: ", n) + + if n == FFI_BAD_CONTEXT then + error("API disabled in the current context", 2) + end + + if n == 0 then + return nil + end + + if n == 1 then + local v = values[0] + return ffi_str(v.data, v.len) + end + + if n > 0 then + local ret = new_tab(n, 0) + for i = 1, n do + local v = values[i - 1] + ret[i] = ffi_str(v.data, v.len) + end + return ret + end + + -- n == FFI_ERROR + error(ffi_str(errmsg[0]), 2) +end + + +do + local mt = new_tab(0, 2) + mt.__newindex = set_resp_header + mt.__index = get_resp_header + + ngx.header = setmetatable(new_tab(0, 0), mt) +end + + +return _M diff --git a/resty/core/shdict.lua b/resty/core/shdict.lua new file mode 100644 index 0000000..e5a2ea2 --- /dev/null +++ b/resty/core/shdict.lua @@ -0,0 +1,638 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local base = require "resty.core.base" + + +local _M = { + version = base.version +} + +local ngx_shared = ngx.shared +if not ngx_shared then + return _M +end + + +local ffi_new = ffi.new +local ffi_str = ffi.string +local C = ffi.C +local get_string_buf = base.get_string_buf +local get_string_buf_size = base.get_string_buf_size +local get_size_ptr = base.get_size_ptr +local tonumber = tonumber +local tostring = tostring +local next = next +local type = type +local error = error +local getmetatable = getmetatable +local FFI_DECLINED = base.FFI_DECLINED +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_shdict_get +local ngx_lua_ffi_shdict_incr +local ngx_lua_ffi_shdict_store +local ngx_lua_ffi_shdict_flush_all +local ngx_lua_ffi_shdict_get_ttl +local ngx_lua_ffi_shdict_set_expire +local ngx_lua_ffi_shdict_capacity +local ngx_lua_ffi_shdict_free_space +local ngx_lua_ffi_shdict_udata_to_zone + + +if subsystem == 'http' then + ffi.cdef[[ +int ngx_http_lua_ffi_shdict_get(void *zone, const unsigned char *key, + size_t key_len, int *value_type, unsigned char **str_value_buf, + size_t *str_value_len, double *num_value, int *user_flags, + int get_stale, int *is_stale, char **errmsg); + +int ngx_http_lua_ffi_shdict_incr(void *zone, const unsigned char *key, + size_t key_len, double *value, char **err, int has_init, + double init, long init_ttl, int *forcible); + +int ngx_http_lua_ffi_shdict_store(void *zone, int op, + const unsigned char *key, size_t key_len, int value_type, + const unsigned char *str_value_buf, size_t str_value_len, + double num_value, long exptime, int user_flags, char **errmsg, + int *forcible); + +int ngx_http_lua_ffi_shdict_flush_all(void *zone); + +long ngx_http_lua_ffi_shdict_get_ttl(void *zone, + const unsigned char *key, size_t key_len); + +int ngx_http_lua_ffi_shdict_set_expire(void *zone, + const unsigned char *key, size_t key_len, long exptime); + +size_t ngx_http_lua_ffi_shdict_capacity(void *zone); + +void *ngx_http_lua_ffi_shdict_udata_to_zone(void *zone_udata); + ]] + + ngx_lua_ffi_shdict_get = C.ngx_http_lua_ffi_shdict_get + ngx_lua_ffi_shdict_incr = C.ngx_http_lua_ffi_shdict_incr + ngx_lua_ffi_shdict_store = C.ngx_http_lua_ffi_shdict_store + ngx_lua_ffi_shdict_flush_all = C.ngx_http_lua_ffi_shdict_flush_all + ngx_lua_ffi_shdict_get_ttl = C.ngx_http_lua_ffi_shdict_get_ttl + ngx_lua_ffi_shdict_set_expire = C.ngx_http_lua_ffi_shdict_set_expire + ngx_lua_ffi_shdict_capacity = C.ngx_http_lua_ffi_shdict_capacity + ngx_lua_ffi_shdict_udata_to_zone = + C.ngx_http_lua_ffi_shdict_udata_to_zone + + if not pcall(function () + return C.ngx_http_lua_ffi_shdict_free_space + end) + then + ffi.cdef[[ +size_t ngx_http_lua_ffi_shdict_free_space(void *zone); + ]] + end + + pcall(function () + ngx_lua_ffi_shdict_free_space = C.ngx_http_lua_ffi_shdict_free_space + end) + +elseif subsystem == 'stream' then + + ffi.cdef[[ +int ngx_stream_lua_ffi_shdict_get(void *zone, const unsigned char *key, + size_t key_len, int *value_type, unsigned char **str_value_buf, + size_t *str_value_len, double *num_value, int *user_flags, + int get_stale, int *is_stale, char **errmsg); + +int ngx_stream_lua_ffi_shdict_incr(void *zone, const unsigned char *key, + size_t key_len, double *value, char **err, int has_init, + double init, long init_ttl, int *forcible); + +int ngx_stream_lua_ffi_shdict_store(void *zone, int op, + const unsigned char *key, size_t key_len, int value_type, + const unsigned char *str_value_buf, size_t str_value_len, + double num_value, long exptime, int user_flags, char **errmsg, + int *forcible); + +int ngx_stream_lua_ffi_shdict_flush_all(void *zone); + +long ngx_stream_lua_ffi_shdict_get_ttl(void *zone, + const unsigned char *key, size_t key_len); + +int ngx_stream_lua_ffi_shdict_set_expire(void *zone, + const unsigned char *key, size_t key_len, long exptime); + +size_t ngx_stream_lua_ffi_shdict_capacity(void *zone); + +void *ngx_stream_lua_ffi_shdict_udata_to_zone(void *zone_udata); + ]] + + ngx_lua_ffi_shdict_get = C.ngx_stream_lua_ffi_shdict_get + ngx_lua_ffi_shdict_incr = C.ngx_stream_lua_ffi_shdict_incr + ngx_lua_ffi_shdict_store = C.ngx_stream_lua_ffi_shdict_store + ngx_lua_ffi_shdict_flush_all = C.ngx_stream_lua_ffi_shdict_flush_all + ngx_lua_ffi_shdict_get_ttl = C.ngx_stream_lua_ffi_shdict_get_ttl + ngx_lua_ffi_shdict_set_expire = C.ngx_stream_lua_ffi_shdict_set_expire + ngx_lua_ffi_shdict_capacity = C.ngx_stream_lua_ffi_shdict_capacity + ngx_lua_ffi_shdict_udata_to_zone = + C.ngx_stream_lua_ffi_shdict_udata_to_zone + + if not pcall(function () + return C.ngx_stream_lua_ffi_shdict_free_space + end) + then + ffi.cdef[[ +size_t ngx_stream_lua_ffi_shdict_free_space(void *zone); + ]] + end + + -- ngx_stream_lua is only compatible with NGINX >= 1.13.6, meaning it + -- cannot lack support for ngx_stream_lua_ffi_shdict_free_space. + ngx_lua_ffi_shdict_free_space = C.ngx_stream_lua_ffi_shdict_free_space + +else + error("unknown subsystem: " .. subsystem) +end + +if not pcall(function () return C.free end) then + ffi.cdef[[ +void free(void *ptr); + ]] +end + + +local value_type = ffi_new("int[1]") +local user_flags = ffi_new("int[1]") +local num_value = ffi_new("double[1]") +local is_stale = ffi_new("int[1]") +local forcible = ffi_new("int[1]") +local str_value_buf = ffi_new("unsigned char *[1]") +local errmsg = base.get_errmsg_ptr() + + +local function check_zone(zone) + if not zone or type(zone) ~= "table" then + error("bad \"zone\" argument", 3) + end + + zone = zone[1] + if type(zone) ~= "userdata" then + error("bad \"zone\" argument", 3) + end + + zone = ngx_lua_ffi_shdict_udata_to_zone(zone) + if zone == nil then + error("bad \"zone\" argument", 3) + end + + return zone +end + + +local function shdict_store(zone, op, key, value, exptime, flags) + zone = check_zone(zone) + + if not exptime then + exptime = 0 + elseif exptime < 0 then + error('bad "exptime" argument', 2) + end + + if not flags then + flags = 0 + end + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + if key_len > 65535 then + return nil, "key too long" + end + + local str_val_buf + local str_val_len = 0 + local num_val = 0 + local valtyp = type(value) + + -- print("value type: ", valtyp) + -- print("exptime: ", exptime) + + if valtyp == "string" then + valtyp = 4 -- LUA_TSTRING + str_val_buf = value + str_val_len = #value + + elseif valtyp == "number" then + valtyp = 3 -- LUA_TNUMBER + num_val = value + + elseif value == nil then + valtyp = 0 -- LUA_TNIL + + elseif valtyp == "boolean" then + valtyp = 1 -- LUA_TBOOLEAN + num_val = value and 1 or 0 + + else + return nil, "bad value type" + end + + local rc = ngx_lua_ffi_shdict_store(zone, op, key, key_len, + valtyp, str_val_buf, + str_val_len, num_val, + exptime * 1000, flags, errmsg, + forcible) + + -- print("rc == ", rc) + + if rc == 0 then -- NGX_OK + return true, nil, forcible[0] == 1 + end + + -- NGX_DECLINED or NGX_ERROR + return false, ffi_str(errmsg[0]), forcible[0] == 1 +end + + +local function shdict_set(zone, key, value, exptime, flags) + return shdict_store(zone, 0, key, value, exptime, flags) +end + + +local function shdict_safe_set(zone, key, value, exptime, flags) + return shdict_store(zone, 0x0004, key, value, exptime, flags) +end + + +local function shdict_add(zone, key, value, exptime, flags) + return shdict_store(zone, 0x0001, key, value, exptime, flags) +end + + +local function shdict_safe_add(zone, key, value, exptime, flags) + return shdict_store(zone, 0x0005, key, value, exptime, flags) +end + + +local function shdict_replace(zone, key, value, exptime, flags) + return shdict_store(zone, 0x0002, key, value, exptime, flags) +end + + +local function shdict_delete(zone, key) + return shdict_set(zone, key, nil) +end + + +local function shdict_get(zone, key) + zone = check_zone(zone) + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + if key_len > 65535 then + return nil, "key too long" + end + + local size = get_string_buf_size() + local buf = get_string_buf(size) + str_value_buf[0] = buf + local value_len = get_size_ptr() + value_len[0] = size + + local rc = ngx_lua_ffi_shdict_get(zone, key, key_len, value_type, + str_value_buf, value_len, + num_value, user_flags, 0, + is_stale, errmsg) + if rc ~= 0 then + if errmsg[0] then + return nil, ffi_str(errmsg[0]) + end + + error("failed to get the key") + end + + local typ = value_type[0] + + if typ == 0 then -- LUA_TNIL + return nil + end + + local flags = tonumber(user_flags[0]) + + local val + + if typ == 4 then -- LUA_TSTRING + if str_value_buf[0] ~= buf then + -- ngx.say("len: ", tonumber(value_len[0])) + buf = str_value_buf[0] + val = ffi_str(buf, value_len[0]) + C.free(buf) + else + val = ffi_str(buf, value_len[0]) + end + + elseif typ == 3 then -- LUA_TNUMBER + val = tonumber(num_value[0]) + + elseif typ == 1 then -- LUA_TBOOLEAN + val = (tonumber(buf[0]) ~= 0) + + else + error("unknown value type: " .. typ) + end + + if flags ~= 0 then + return val, flags + end + + return val +end + + +local function shdict_get_stale(zone, key) + zone = check_zone(zone) + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + if key_len > 65535 then + return nil, "key too long" + end + + local size = get_string_buf_size() + local buf = get_string_buf(size) + str_value_buf[0] = buf + local value_len = get_size_ptr() + value_len[0] = size + + local rc = ngx_lua_ffi_shdict_get(zone, key, key_len, value_type, + str_value_buf, value_len, + num_value, user_flags, 1, + is_stale, errmsg) + if rc ~= 0 then + if errmsg[0] then + return nil, ffi_str(errmsg[0]) + end + + error("failed to get the key") + end + + local typ = value_type[0] + + if typ == 0 then -- LUA_TNIL + return nil + end + + local flags = tonumber(user_flags[0]) + local val + + if typ == 4 then -- LUA_TSTRING + if str_value_buf[0] ~= buf then + -- ngx.say("len: ", tonumber(value_len[0])) + buf = str_value_buf[0] + val = ffi_str(buf, value_len[0]) + C.free(buf) + else + val = ffi_str(buf, value_len[0]) + end + + elseif typ == 3 then -- LUA_TNUMBER + val = tonumber(num_value[0]) + + elseif typ == 1 then -- LUA_TBOOLEAN + val = (tonumber(buf[0]) ~= 0) + + else + error("unknown value type: " .. typ) + end + + if flags ~= 0 then + return val, flags, is_stale[0] == 1 + end + + return val, nil, is_stale[0] == 1 +end + + +local function shdict_incr(zone, key, value, init, init_ttl) + zone = check_zone(zone) + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + if key_len > 65535 then + return nil, "key too long" + end + + if type(value) ~= "number" then + value = tonumber(value) + end + num_value[0] = value + + if init then + local typ = type(init) + if typ ~= "number" then + init = tonumber(init) + + if not init then + error("bad init arg: number expected, got " .. typ, 2) + end + end + end + + if init_ttl ~= nil then + local typ = type(init_ttl) + if typ ~= "number" then + init_ttl = tonumber(init_ttl) + + if not init_ttl then + error("bad init_ttl arg: number expected, got " .. typ, 2) + end + end + + if init_ttl < 0 then + error('bad "init_ttl" argument', 2) + end + + if not init then + error('must provide "init" when providing "init_ttl"', 2) + end + + else + init_ttl = 0 + end + + local rc = ngx_lua_ffi_shdict_incr(zone, key, key_len, num_value, + errmsg, init and 1 or 0, + init or 0, init_ttl * 1000, + forcible) + if rc ~= 0 then -- ~= NGX_OK + return nil, ffi_str(errmsg[0]) + end + + if not init then + return tonumber(num_value[0]) + end + + return tonumber(num_value[0]), nil, forcible[0] == 1 +end + + +local function shdict_flush_all(zone) + zone = check_zone(zone) + + ngx_lua_ffi_shdict_flush_all(zone) +end + + +local function shdict_ttl(zone, key) + zone = check_zone(zone) + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + + if key_len > 65535 then + return nil, "key too long" + end + + local rc = ngx_lua_ffi_shdict_get_ttl(zone, key, key_len) + + if rc == FFI_DECLINED then + return nil, "not found" + end + + return tonumber(rc) / 1000 +end + + +local function shdict_expire(zone, key, exptime) + zone = check_zone(zone) + + if not exptime then + error('bad "exptime" argument', 2) + end + + if key == nil then + return nil, "nil key" + end + + if type(key) ~= "string" then + key = tostring(key) + end + + local key_len = #key + if key_len == 0 then + return nil, "empty key" + end + + if key_len > 65535 then + return nil, "key too long" + end + + local rc = ngx_lua_ffi_shdict_set_expire(zone, key, key_len, + exptime * 1000) + + if rc == FFI_DECLINED then + return nil, "not found" + end + + -- NGINX_OK/FFI_OK + + return true +end + + +local function shdict_capacity(zone) + zone = check_zone(zone) + + return tonumber(ngx_lua_ffi_shdict_capacity(zone)) +end + + +local shdict_free_space +if ngx_lua_ffi_shdict_free_space then + shdict_free_space = function (zone) + zone = check_zone(zone) + + return tonumber(ngx_lua_ffi_shdict_free_space(zone)) + end + +else + shdict_free_space = function () + error("'shm:free_space()' not supported in NGINX < 1.11.7", 2) + end +end + + +local _, dict = next(ngx_shared, nil) +if dict then + local mt = getmetatable(dict) + if mt then + mt = mt.__index + if mt then + mt.get = shdict_get + mt.get_stale = shdict_get_stale + mt.incr = shdict_incr + mt.set = shdict_set + mt.safe_set = shdict_safe_set + mt.add = shdict_add + mt.safe_add = shdict_safe_add + mt.replace = shdict_replace + mt.delete = shdict_delete + mt.flush_all = shdict_flush_all + mt.ttl = shdict_ttl + mt.expire = shdict_expire + mt.capacity = shdict_capacity + mt.free_space = shdict_free_space + end + end +end + + +return _M diff --git a/resty/core/time.lua b/resty/core/time.lua new file mode 100644 index 0000000..10ae72e --- /dev/null +++ b/resty/core/time.lua @@ -0,0 +1,159 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require 'ffi' +local base = require "resty.core.base" + + +local error = error +local tonumber = tonumber +local type = type +local C = ffi.C +local ffi_new = ffi.new +local ffi_str = ffi.string +local time_val = ffi_new("long[1]") +local get_string_buf = base.get_string_buf +local ngx = ngx +local FFI_ERROR = base.FFI_ERROR +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_now +local ngx_lua_ffi_time +local ngx_lua_ffi_today +local ngx_lua_ffi_localtime +local ngx_lua_ffi_utctime +local ngx_lua_ffi_update_time + + +if subsystem == 'http' then + ffi.cdef[[ +double ngx_http_lua_ffi_now(void); +long ngx_http_lua_ffi_time(void); +void ngx_http_lua_ffi_today(unsigned char *buf); +void ngx_http_lua_ffi_localtime(unsigned char *buf); +void ngx_http_lua_ffi_utctime(unsigned char *buf); +void ngx_http_lua_ffi_update_time(void); +int ngx_http_lua_ffi_cookie_time(unsigned char *buf, long t); +void ngx_http_lua_ffi_http_time(unsigned char *buf, long t); +void ngx_http_lua_ffi_parse_http_time(const unsigned char *str, size_t len, + long *time); + ]] + + ngx_lua_ffi_now = C.ngx_http_lua_ffi_now + ngx_lua_ffi_time = C.ngx_http_lua_ffi_time + ngx_lua_ffi_today = C.ngx_http_lua_ffi_today + ngx_lua_ffi_localtime = C.ngx_http_lua_ffi_localtime + ngx_lua_ffi_utctime = C.ngx_http_lua_ffi_utctime + ngx_lua_ffi_update_time = C.ngx_http_lua_ffi_update_time + +elseif subsystem == 'stream' then + ffi.cdef[[ +double ngx_stream_lua_ffi_now(void); +long ngx_stream_lua_ffi_time(void); +void ngx_stream_lua_ffi_today(unsigned char *buf); +void ngx_stream_lua_ffi_localtime(unsigned char *buf); +void ngx_stream_lua_ffi_utctime(unsigned char *buf); +void ngx_stream_lua_ffi_update_time(void); + ]] + + ngx_lua_ffi_now = C.ngx_stream_lua_ffi_now + ngx_lua_ffi_time = C.ngx_stream_lua_ffi_time + ngx_lua_ffi_today = C.ngx_stream_lua_ffi_today + ngx_lua_ffi_localtime = C.ngx_stream_lua_ffi_localtime + ngx_lua_ffi_utctime = C.ngx_stream_lua_ffi_utctime + ngx_lua_ffi_update_time = C.ngx_stream_lua_ffi_update_time +end + + +function ngx.now() + return tonumber(ngx_lua_ffi_now()) +end + + +function ngx.time() + return tonumber(ngx_lua_ffi_time()) +end + + +function ngx.update_time() + ngx_lua_ffi_update_time() +end + + +function ngx.today() + -- the format of today is 2010-11-19 + local today_buf_size = 10 + local buf = get_string_buf(today_buf_size) + ngx_lua_ffi_today(buf) + return ffi_str(buf, today_buf_size) +end + + +function ngx.localtime() + -- the format of localtime is 2010-11-19 20:56:31 + local localtime_buf_size = 19 + local buf = get_string_buf(localtime_buf_size) + ngx_lua_ffi_localtime(buf) + return ffi_str(buf, localtime_buf_size) +end + + +function ngx.utctime() + -- the format of utctime is 2010-11-19 20:56:31 + local utctime_buf_size = 19 + local buf = get_string_buf(utctime_buf_size) + ngx_lua_ffi_utctime(buf) + return ffi_str(buf, utctime_buf_size) +end + + +if subsystem == 'http' then + +function ngx.cookie_time(sec) + if type(sec) ~= "number" then + error("number argument only", 2) + end + + -- the format of cookie time is Mon, 28-Sep-2038 06:00:00 GMT + -- or Mon, 28-Sep-18 06:00:00 GMT + local cookie_time_buf_size = 29 + local buf = get_string_buf(cookie_time_buf_size) + local used_size = C.ngx_http_lua_ffi_cookie_time(buf, sec) + return ffi_str(buf, used_size) +end + + +function ngx.http_time(sec) + if type(sec) ~= "number" then + error("number argument only", 2) + end + + -- the format of http time is Mon, 28 Sep 1970 06:00:00 GMT + local http_time_buf_size = 29 + local buf = get_string_buf(http_time_buf_size) + C.ngx_http_lua_ffi_http_time(buf, sec) + return ffi_str(buf, http_time_buf_size) +end + + +function ngx.parse_http_time(time_str) + if type(time_str) ~= "string" then + error("string argument only", 2) + end + + C.ngx_http_lua_ffi_parse_http_time(time_str, #time_str, time_val) + + local res = time_val[0] + if res == FFI_ERROR then + return nil + end + + return tonumber(res) +end + +end + +return { + version = base.version +} diff --git a/resty/core/uri.lua b/resty/core/uri.lua new file mode 100644 index 0000000..96b1ab4 --- /dev/null +++ b/resty/core/uri.lua @@ -0,0 +1,115 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_string = ffi.string +local ngx = ngx +local type = type +local error = error +local tostring = tostring +local get_string_buf = base.get_string_buf +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_escape_uri +local ngx_lua_ffi_unescape_uri +local ngx_lua_ffi_uri_escaped_length + +local NGX_ESCAPE_URI = 0 +local NGX_ESCAPE_URI_COMPONENT = 2 +local NGX_ESCAPE_MAIL_AUTH = 6 + + +if subsystem == "http" then + ffi.cdef[[ + size_t ngx_http_lua_ffi_uri_escaped_length(const unsigned char *src, + size_t len, int type); + + void ngx_http_lua_ffi_escape_uri(const unsigned char *src, size_t len, + unsigned char *dst, int type); + + size_t ngx_http_lua_ffi_unescape_uri(const unsigned char *src, + size_t len, unsigned char *dst); + ]] + + ngx_lua_ffi_escape_uri = C.ngx_http_lua_ffi_escape_uri + ngx_lua_ffi_unescape_uri = C.ngx_http_lua_ffi_unescape_uri + ngx_lua_ffi_uri_escaped_length = C.ngx_http_lua_ffi_uri_escaped_length + +elseif subsystem == "stream" then + ffi.cdef[[ + size_t ngx_stream_lua_ffi_uri_escaped_length(const unsigned char *src, + size_t len, int type); + + void ngx_stream_lua_ffi_escape_uri(const unsigned char *src, size_t len, + unsigned char *dst, int type); + + size_t ngx_stream_lua_ffi_unescape_uri(const unsigned char *src, + size_t len, unsigned char *dst); + ]] + + ngx_lua_ffi_escape_uri = C.ngx_stream_lua_ffi_escape_uri + ngx_lua_ffi_unescape_uri = C.ngx_stream_lua_ffi_unescape_uri + ngx_lua_ffi_uri_escaped_length = C.ngx_stream_lua_ffi_uri_escaped_length +end + + +ngx.escape_uri = function (s, esc_type) + if type(s) ~= 'string' then + if not s then + s = '' + + else + s = tostring(s) + end + end + + if esc_type == nil then + esc_type = NGX_ESCAPE_URI_COMPONENT + + else + if type(esc_type) ~= 'number' then + error("\"type\" is not a number", 3) + end + + if esc_type < NGX_ESCAPE_URI or esc_type > NGX_ESCAPE_MAIL_AUTH then + error("\"type\" " .. esc_type .. " out of range", 3) + end + end + + local slen = #s + local dlen = ngx_lua_ffi_uri_escaped_length(s, slen, esc_type) + + -- print("dlen: ", tonumber(dlen)) + if dlen == slen then + return s + end + local dst = get_string_buf(dlen) + ngx_lua_ffi_escape_uri(s, slen, dst, esc_type) + return ffi_string(dst, dlen) +end + + +ngx.unescape_uri = function (s) + if type(s) ~= 'string' then + if not s then + s = '' + else + s = tostring(s) + end + end + local slen = #s + local dlen = slen + local dst = get_string_buf(dlen) + dlen = ngx_lua_ffi_unescape_uri(s, slen, dst) + return ffi_string(dst, dlen) +end + + +return { + version = base.version, +} diff --git a/resty/core/utils.lua b/resty/core/utils.lua new file mode 100644 index 0000000..398d7d5 --- /dev/null +++ b/resty/core/utils.lua @@ -0,0 +1,44 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" +base.allows_subsystem("http") + + +local C = ffi.C +local ffi_str = ffi.string +local ffi_copy = ffi.copy +local byte = string.byte +local str_find = string.find +local get_string_buf = base.get_string_buf + + +ffi.cdef[[ + void ngx_http_lua_ffi_str_replace_char(unsigned char *buf, size_t len, + const unsigned char find, const unsigned char replace); +]] + + +local _M = { + version = base.version +} + + +function _M.str_replace_char(str, find, replace) + if not str_find(str, find, nil, true) then + return str + end + + local len = #str + local buf = get_string_buf(len) + ffi_copy(buf, str) + + C.ngx_http_lua_ffi_str_replace_char(buf, len, byte(find), + byte(replace)) + + return ffi_str(buf, len) +end + + +return _M diff --git a/resty/core/var.lua b/resty/core/var.lua new file mode 100644 index 0000000..ea9c763 --- /dev/null +++ b/resty/core/var.lua @@ -0,0 +1,160 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local ffi_new = ffi.new +local ffi_str = ffi.string +local type = type +local error = error +local tostring = tostring +local setmetatable = setmetatable +local get_request = base.get_request +local get_string_buf = base.get_string_buf +local get_size_ptr = base.get_size_ptr +local new_tab = base.new_tab +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_var_get +local ngx_lua_ffi_var_set + + +local ERR_BUF_SIZE = 256 + + +ngx.var = new_tab(0, 0) + + +if subsystem == "http" then + ffi.cdef[[ + int ngx_http_lua_ffi_var_get(ngx_http_request_t *r, + const char *name_data, size_t name_len, char *lowcase_buf, + int capture_id, char **value, size_t *value_len, char **err); + + int ngx_http_lua_ffi_var_set(ngx_http_request_t *r, + const unsigned char *name_data, size_t name_len, + unsigned char *lowcase_buf, const unsigned char *value, + size_t value_len, unsigned char *errbuf, size_t *errlen); + ]] + + ngx_lua_ffi_var_get = C.ngx_http_lua_ffi_var_get + ngx_lua_ffi_var_set = C.ngx_http_lua_ffi_var_set + +elseif subsystem == "stream" then + ffi.cdef[[ + int ngx_stream_lua_ffi_var_get(ngx_stream_lua_request_t *r, + const char *name_data, size_t name_len, char *lowcase_buf, + int capture_id, char **value, size_t *value_len, char **err); + + int ngx_stream_lua_ffi_var_set(ngx_stream_lua_request_t *r, + const unsigned char *name_data, size_t name_len, + unsigned char *lowcase_buf, const unsigned char *value, + size_t value_len, unsigned char *errbuf, size_t *errlen); + ]] + + ngx_lua_ffi_var_get = C.ngx_stream_lua_ffi_var_get + ngx_lua_ffi_var_set = C.ngx_stream_lua_ffi_var_set +end + + +local value_ptr = ffi_new("unsigned char *[1]") +local errmsg = base.get_errmsg_ptr() + + +local function var_get(self, name) + local r = get_request() + if not r then + error("no request found") + end + + local value_len = get_size_ptr() + local rc + if type(name) == "number" then + rc = ngx_lua_ffi_var_get(r, nil, 0, nil, name, value_ptr, value_len, + errmsg) + + else + if type(name) ~= "string" then + error("bad variable name", 2) + end + + local name_len = #name + local lowcase_buf = get_string_buf(name_len) + + rc = ngx_lua_ffi_var_get(r, name, name_len, lowcase_buf, 0, value_ptr, + value_len, errmsg) + end + + -- ngx.log(ngx.WARN, "rc = ", rc) + + if rc == 0 then -- NGX_OK + return ffi_str(value_ptr[0], value_len[0]) + end + + if rc == -5 then -- NGX_DECLINED + return nil + end + + if rc == -1 then -- NGX_ERROR + error(ffi_str(errmsg[0]), 2) + end +end + + +local function var_set(self, name, value) + local r = get_request() + if not r then + error("no request found") + end + + if type(name) ~= "string" then + error("bad variable name", 2) + end + local name_len = #name + + local errlen = get_size_ptr() + errlen[0] = ERR_BUF_SIZE + local lowcase_buf = get_string_buf(name_len + ERR_BUF_SIZE) + + local value_len + if value == nil then + value_len = 0 + else + if type(value) ~= 'string' then + value = tostring(value) + end + value_len = #value + end + + local errbuf = lowcase_buf + name_len + local rc = ngx_lua_ffi_var_set(r, name, name_len, lowcase_buf, value, + value_len, errbuf, errlen) + + -- ngx.log(ngx.WARN, "rc = ", rc) + + if rc == 0 then -- NGX_OK + return + end + + if rc == -1 then -- NGX_ERROR + error(ffi_str(errbuf, errlen[0]), 2) + end +end + + +do + local mt = new_tab(0, 2) + mt.__index = var_get + mt.__newindex = var_set + + setmetatable(ngx.var, mt) +end + + +return { + version = base.version +} diff --git a/resty/core/worker.lua b/resty/core/worker.lua new file mode 100644 index 0000000..c336deb --- /dev/null +++ b/resty/core/worker.lua @@ -0,0 +1,77 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local base = require "resty.core.base" + + +local C = ffi.C +local new_tab = base.new_tab +local subsystem = ngx.config.subsystem + + +local ngx_lua_ffi_worker_id +local ngx_lua_ffi_worker_pid +local ngx_lua_ffi_worker_count +local ngx_lua_ffi_worker_exiting + + +ngx.worker = new_tab(0, 4) + + +if subsystem == "http" then + ffi.cdef[[ + int ngx_http_lua_ffi_worker_id(void); + int ngx_http_lua_ffi_worker_pid(void); + int ngx_http_lua_ffi_worker_count(void); + int ngx_http_lua_ffi_worker_exiting(void); + ]] + + ngx_lua_ffi_worker_id = C.ngx_http_lua_ffi_worker_id + ngx_lua_ffi_worker_pid = C.ngx_http_lua_ffi_worker_pid + ngx_lua_ffi_worker_count = C.ngx_http_lua_ffi_worker_count + ngx_lua_ffi_worker_exiting = C.ngx_http_lua_ffi_worker_exiting + +elseif subsystem == "stream" then + ffi.cdef[[ + int ngx_stream_lua_ffi_worker_id(void); + int ngx_stream_lua_ffi_worker_pid(void); + int ngx_stream_lua_ffi_worker_count(void); + int ngx_stream_lua_ffi_worker_exiting(void); + ]] + + ngx_lua_ffi_worker_id = C.ngx_stream_lua_ffi_worker_id + ngx_lua_ffi_worker_pid = C.ngx_stream_lua_ffi_worker_pid + ngx_lua_ffi_worker_count = C.ngx_stream_lua_ffi_worker_count + ngx_lua_ffi_worker_exiting = C.ngx_stream_lua_ffi_worker_exiting +end + + +function ngx.worker.exiting() + return ngx_lua_ffi_worker_exiting() ~= 0 +end + + +function ngx.worker.pid() + return ngx_lua_ffi_worker_pid() +end + + +function ngx.worker.id() + local id = ngx_lua_ffi_worker_id() + if id < 0 then + return nil + end + + return id +end + + +function ngx.worker.count() + return ngx_lua_ffi_worker_count() +end + + +return { + _VERSION = base.version +} diff --git a/resty/lrucache.lua b/resty/lrucache.lua new file mode 100644 index 0000000..147842a --- /dev/null +++ b/resty/lrucache.lua @@ -0,0 +1,338 @@ +-- Copyright (C) Yichun Zhang (agentzh) + + +local ffi = require "ffi" +local ffi_new = ffi.new +local ffi_sizeof = ffi.sizeof +local ffi_cast = ffi.cast +local ffi_fill = ffi.fill +local ngx_now = ngx.now +local uintptr_t = ffi.typeof("uintptr_t") +local setmetatable = setmetatable +local tonumber = tonumber +local type = type +local new_tab +do + local ok + ok, new_tab = pcall(require, "table.new") + if not ok then + new_tab = function(narr, nrec) return {} end + end +end + + +if string.find(jit.version, " 2.0", 1, true) then + ngx.log(ngx.ALERT, "use of lua-resty-lrucache with LuaJIT 2.0 is ", + "not recommended; use LuaJIT 2.1+ instead") +end + + +local ok, tb_clear = pcall(require, "table.clear") +if not ok then + local pairs = pairs + tb_clear = function (tab) + for k, _ in pairs(tab) do + tab[k] = nil + end + end +end + + +-- queue data types +-- +-- this queue is a double-ended queue and the first node +-- is reserved for the queue itself. +-- the implementation is mostly borrowed from nginx's ngx_queue_t data +-- structure. + +ffi.cdef[[ + typedef struct lrucache_queue_s lrucache_queue_t; + struct lrucache_queue_s { + double expire; /* in seconds */ + lrucache_queue_t *prev; + lrucache_queue_t *next; + uint32_t user_flags; + }; +]] + +local queue_arr_type = ffi.typeof("lrucache_queue_t[?]") +local queue_type = ffi.typeof("lrucache_queue_t") +local NULL = ffi.null + + +-- queue utility functions + +local function queue_insert_tail(h, x) + local last = h[0].prev + x.prev = last + last.next = x + x.next = h + h[0].prev = x +end + + +local function queue_init(size) + if not size then + size = 0 + end + local q = ffi_new(queue_arr_type, size + 1) + ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) + + if size == 0 then + q[0].prev = q + q[0].next = q + + else + local prev = q[0] + for i = 1, size do + local e = q + i + e.user_flags = 0 + prev.next = e + e.prev = prev + prev = e + end + + local last = q[size] + last.next = q + q[0].prev = last + end + + return q +end + + +local function queue_is_empty(q) + -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) + return q == q[0].prev +end + + +local function queue_remove(x) + local prev = x.prev + local next = x.next + + next.prev = prev + prev.next = next + + -- for debugging purpose only: + x.prev = NULL + x.next = NULL +end + + +local function queue_insert_head(h, x) + x.next = h[0].next + x.next.prev = x + x.prev = h + h[0].next = x +end + + +local function queue_last(h) + return h[0].prev +end + + +local function queue_head(h) + return h[0].next +end + + +-- true module stuffs + +local _M = { + _VERSION = '0.10' +} +local mt = { __index = _M } + + +local function ptr2num(ptr) + return tonumber(ffi_cast(uintptr_t, ptr)) +end + + +function _M.new(size) + if size < 1 then + return nil, "size too small" + end + + local self = { + hasht = {}, + free_queue = queue_init(size), + cache_queue = queue_init(), + key2node = {}, + node2key = {}, + num_items = 0, + max_items = size, + } + return setmetatable(self, mt) +end + + +function _M.count(self) + return self.num_items +end + + +function _M.capacity(self) + return self.max_items +end + + +function _M.get(self, key) + local hasht = self.hasht + local val = hasht[key] + if val == nil then + return nil + end + + local node = self.key2node[key] + + -- print(key, ": moving node ", tostring(node), " to cache queue head") + local cache_queue = self.cache_queue + queue_remove(node) + queue_insert_head(cache_queue, node) + + if node.expire >= 0 and node.expire < ngx_now() then + -- print("expired: ", node.expire, " > ", ngx_now()) + return nil, val, node.user_flags + end + + return val, nil, node.user_flags +end + + +function _M.delete(self, key) + self.hasht[key] = nil + + local key2node = self.key2node + local node = key2node[key] + + if not node then + return false + end + + key2node[key] = nil + self.node2key[ptr2num(node)] = nil + + queue_remove(node) + queue_insert_tail(self.free_queue, node) + self.num_items = self.num_items - 1 + return true +end + + +function _M.set(self, key, value, ttl, flags) + local hasht = self.hasht + hasht[key] = value + + local key2node = self.key2node + local node = key2node[key] + if not node then + local free_queue = self.free_queue + local node2key = self.node2key + + if queue_is_empty(free_queue) then + -- evict the least recently used key + -- assert(not queue_is_empty(self.cache_queue)) + node = queue_last(self.cache_queue) + + local oldkey = node2key[ptr2num(node)] + -- print(key, ": evicting oldkey: ", oldkey, ", oldnode: ", + -- tostring(node)) + if oldkey then + hasht[oldkey] = nil + key2node[oldkey] = nil + end + + else + -- take a free queue node + node = queue_head(free_queue) + -- only add count if we are not evicting + self.num_items = self.num_items + 1 + -- print(key, ": get a new free node: ", tostring(node)) + end + + node2key[ptr2num(node)] = key + key2node[key] = node + end + + queue_remove(node) + queue_insert_head(self.cache_queue, node) + + if ttl then + node.expire = ngx_now() + ttl + else + node.expire = -1 + end + + if type(flags) == "number" and flags >= 0 then + node.user_flags = flags + + else + node.user_flags = 0 + end +end + + +function _M.get_keys(self, max_count, res) + if not max_count or max_count == 0 then + max_count = self.num_items + end + + if not res then + res = new_tab(max_count + 1, 0) -- + 1 for trailing hole + end + + local cache_queue = self.cache_queue + local node2key = self.node2key + + local i = 0 + local node = queue_head(cache_queue) + + while node ~= cache_queue do + if i >= max_count then + break + end + + i = i + 1 + res[i] = node2key[ptr2num(node)] + node = node.next + end + + res[i + 1] = nil + + return res +end + + +function _M.flush_all(self) + tb_clear(self.hasht) + tb_clear(self.node2key) + tb_clear(self.key2node) + + local cache_queue = self.cache_queue + local free_queue = self.free_queue + + -- splice the cache_queue into free_queue + if not queue_is_empty(cache_queue) then + local free_head = free_queue[0] + local free_last = free_head.prev + + local cache_head = cache_queue[0] + local cache_first = cache_head.next + local cache_last = cache_head.prev + + free_last.next = cache_first + cache_first.prev = free_last + + cache_last.next = free_head + free_head.prev = cache_last + + cache_head.next = cache_queue + cache_head.prev = cache_queue + end +end + + +return _M diff --git a/resty/lrucache/pureffi.lua b/resty/lrucache/pureffi.lua new file mode 100644 index 0000000..fc1103e --- /dev/null +++ b/resty/lrucache/pureffi.lua @@ -0,0 +1,606 @@ +-- Copyright (C) Yichun Zhang (agentzh) +-- Copyright (C) Shuxin Yang + +--[[ + This module implements a key/value cache store. We adopt LRU as our +replace/evict policy. Each key/value pair is tagged with a Time-to-Live (TTL); +from user's perspective, stale pairs are automatically removed from the cache. + +Why FFI +------- + In Lua, expression "table[key] = nil" does not *PHYSICALLY* remove the value +associated with the key; it just set the value to be nil! So the table will +keep growing with large number of the key/nil pairs which will be purged until +resize() operator is called. + + This "feature" is terribly ill-suited to what we need. Therefore we have to +rely on FFI to build a hash-table where any entry can be physically deleted +immediately. + +Under the hood: +-------------- + In concept, we introduce three data structures to implement the cache store: + 1. key/value vector for storing keys and values. + 2. a queue to mimic the LRU. + 3. hash-table for looking up the value for a given key. + + Unfortunately, efficiency and clarity usually come at each other cost. The +data strucutres we are using are slightly more complicated than what we +described above. + + o. Lua does not have efficient way to store a vector of pair. So, we use + two vectors for key/value pair: one for keys and the other for values + (_M.key_v and _M.val_v, respectively), and i-th key corresponds to + i-th value. + + A key/value pair is identified by the "id" field in a "node" (we shall + discuss node later) + + o. The queue is nothing more than a doubly-linked list of "node" linked via + lrucache_pureffi_queue_s::{next|prev} fields. + + o. The hash-table has two parts: + - the _M.bucket_v[] a vector of bucket, indiced by hash-value, and + - a bucket is a singly-linked list of "node" via the + lrucache_pureffi_queue_s::conflict field. + + A key must be a string, and the hash value of a key is evaluated by: + crc32(key-cast-to-pointer) % size(_M.bucket_v). + We mandate size(_M.bucket_v) being a power-of-two in order to avoid + expensive modulo operation. + + At the heart of the module is an array of "node" (of type + lrucache_pureffi_queue_s). A node: + - keeps the meta-data of its corresponding key/value pair + (embodied by the "id", and "expire" field); + - is a part of LRU queue (embodied by "prev" and "next" fields); + - is a part of hash-table (embodied by the "conflict" field). +]] + +local ffi = require "ffi" +local bit = require "bit" + + +local ffi_new = ffi.new +local ffi_sizeof = ffi.sizeof +local ffi_cast = ffi.cast +local ffi_fill = ffi.fill +local ngx_now = ngx.now +local uintptr_t = ffi.typeof("uintptr_t") +local c_str_t = ffi.typeof("const char*") +local int_t = ffi.typeof("int") +local int_array_t = ffi.typeof("int[?]") + + +local crc_tab = ffi.new("const unsigned int[256]", { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }); + +local setmetatable = setmetatable +local tonumber = tonumber +local tostring = tostring +local type = type + +local brshift = bit.rshift +local bxor = bit.bxor +local band = bit.band + +local new_tab +do + local ok + ok, new_tab = pcall(require, "table.new") + if not ok then + new_tab = function(narr, nrec) return {} end + end +end + +-- queue data types +-- +-- this queue is a double-ended queue and the first node +-- is reserved for the queue itself. +-- the implementation is mostly borrowed from nginx's ngx_queue_t data +-- structure. + +ffi.cdef[[ + /* A lrucache_pureffi_queue_s node hook together three data structures: + * o. the key/value store as embodied by the "id" (which is in essence the + * indentifier of key/pair pair) and the "expire" (which is a metadata + * of the corresponding key/pair pair). + * o. The LRU queue via the prev/next fields. + * o. The hash-tabble as embodied by the "conflict" field. + */ + typedef struct lrucache_pureffi_queue_s lrucache_pureffi_queue_t; + struct lrucache_pureffi_queue_s { + /* Each node is assigned a unique ID at construction time, and the + * ID remain immutatble, regardless the node is in active-list or + * free-list. The queue header is assigned ID 0. Since queue-header + * is a sentinel node, 0 denodes "invalid ID". + * + * Intuitively, we can view the "id" as the identifier of key/value + * pair. + */ + int id; + + /* The bucket of the hash-table is implemented as a singly-linked list. + * The "conflict" refers to the ID of the next node in the bucket. + */ + int conflict; + + uint32_t user_flags; + + double expire; /* in seconds */ + + lrucache_pureffi_queue_t *prev; + lrucache_pureffi_queue_t *next; + }; +]] + +local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]") +--local queue_ptr_type = ffi.typeof("lrucache_pureffi_queue_t*") +local queue_type = ffi.typeof("lrucache_pureffi_queue_t") +local NULL = ffi.null + + +--======================================================================== +-- +-- Queue utility functions +-- +--======================================================================== + +-- Append the element "x" to the given queue "h". +local function queue_insert_tail(h, x) + local last = h[0].prev + x.prev = last + last.next = x + x.next = h + h[0].prev = x +end + + +--[[ +Allocate a queue with size + 1 elements. Elements are linked together in a +circular way, i.e. the last element's "next" points to the first element, +while the first element's "prev" element points to the last element. +]] +local function queue_init(size) + if not size then + size = 0 + end + local q = ffi_new(queue_arr_type, size + 1) + ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) + + if size == 0 then + q[0].prev = q + q[0].next = q + + else + local prev = q[0] + for i = 1, size do + local e = q[i] + e.id = i + e.user_flags = 0 + prev.next = e + e.prev = prev + prev = e + end + + local last = q[size] + last.next = q + q[0].prev = last + end + + return q +end + + +local function queue_is_empty(q) + -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) + return q == q[0].prev +end + + +local function queue_remove(x) + local prev = x.prev + local next = x.next + + next.prev = prev + prev.next = next + + -- for debugging purpose only: + x.prev = NULL + x.next = NULL +end + + +-- Insert the element "x" the to the given queue "h" +local function queue_insert_head(h, x) + x.next = h[0].next + x.next.prev = x + x.prev = h + h[0].next = x +end + + +local function queue_last(h) + return h[0].prev +end + + +local function queue_head(h) + return h[0].next +end + + +--======================================================================== +-- +-- Miscellaneous Utility Functions +-- +--======================================================================== + +local function ptr2num(ptr) + return tonumber(ffi_cast(uintptr_t, ptr)) +end + + +local function crc32_ptr(ptr) + local p = brshift(ptr2num(ptr), 3) + local b = band(p, 255) + local crc32 = crc_tab[b] + + b = band(brshift(p, 8), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + b = band(brshift(p, 16), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + --b = band(brshift(p, 24), 255) + --crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + return crc32 +end + + +--======================================================================== +-- +-- Implementation of "export" functions +-- +--======================================================================== + +local _M = { + _VERSION = '0.10' +} +local mt = { __index = _M } + + +-- "size" specifies the maximum number of entries in the LRU queue, and the +-- "load_factor" designates the 'load factor' of the hash-table we are using +-- internally. The default value of load-factor is 0.5 (i.e. 50%); if the +-- load-factor is specified, it will be clamped to the range of [0.1, 1](i.e. +-- if load-factor is greater than 1, it will be saturated to 1, likewise, +-- if load-factor is smaller than 0.1, it will be clamped to 0.1). +function _M.new(size, load_factor) + if size < 1 then + return nil, "size too small" + end + + -- Determine bucket size, which must be power of two. + local load_f = load_factor + if not load_factor then + load_f = 0.5 + elseif load_factor > 1 then + load_f = 1 + elseif load_factor < 0.1 then + load_f = 0.1 + end + + local bs_min = size / load_f + -- The bucket_sz *MUST* be a power-of-two. See the hash_string(). + local bucket_sz = 1 + repeat + bucket_sz = bucket_sz * 2 + until bucket_sz >= bs_min + + local self = { + size = size, + bucket_sz = bucket_sz, + free_queue = queue_init(size), + cache_queue = queue_init(0), + node_v = nil, + key_v = new_tab(size, 0), + val_v = new_tab(size, 0), + bucket_v = ffi_new(int_array_t, bucket_sz), + num_items = 0, + } + -- "note_v" is an array of all the nodes used in the LRU queue. Exprpession + -- node_v[i] evaluates to the element of ID "i". + self.node_v = self.free_queue + + -- Allocate the array-part of the key_v, val_v, bucket_v. + --local key_v = self.key_v + --local val_v = self.val_v + --local bucket_v = self.bucket_v + ffi_fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0) + + return setmetatable(self, mt) +end + + +function _M.count(self) + return self.num_items +end + + +function _M.capacity(self) + return self.size +end + + +local function hash_string(self, str) + local c_str = ffi_cast(c_str_t, str) + + local hv = crc32_ptr(c_str) + hv = band(hv, self.bucket_sz - 1) + -- Hint: bucket is 0-based + return hv +end + + +-- Search the node associated with the key in the bucket, if found returns +-- the the id of the node, and the id of its previous node in the conflict list. +-- The "bucket_hdr_id" is the ID of the first node in the bucket +local function _find_node_in_bucket(key, key_v, node_v, bucket_hdr_id) + if bucket_hdr_id ~= 0 then + local prev = 0 + local cur = bucket_hdr_id + + while cur ~= 0 and key_v[cur] ~= key do + prev = cur + cur = node_v[cur].conflict + end + + if cur ~= 0 then + return cur, prev + end + end +end + + +-- Return the node corresponding to the key/val. +local function find_key(self, key) + local key_hash = hash_string(self, key) + return _find_node_in_bucket(key, self.key_v, self.node_v, + self.bucket_v[key_hash]) +end + + +--[[ This function tries to + 1. Remove the given key and the associated value from the key/value store, + 2. Remove the entry associated with the key from the hash-table. + + NOTE: all queues remain intact. + + If there was a node bound to the key/val, return that node; otherwise, + nil is returned. +]] +local function remove_key(self, key) + local key_v = self.key_v + local val_v = self.val_v + local node_v = self.node_v + local bucket_v = self.bucket_v + + local key_hash = hash_string(self, key) + local cur, prev = + _find_node_in_bucket(key, key_v, node_v, bucket_v[key_hash]) + + if cur then + -- In an attempt to make key and val dead. + key_v[cur] = nil + val_v[cur] = nil + self.num_items = self.num_items - 1 + + -- Remove the node from the hash table + local next_node = node_v[cur].conflict + if prev ~= 0 then + node_v[prev].conflict = next_node + else + bucket_v[key_hash] = next_node + end + node_v[cur].conflict = 0 + + return cur + end +end + + +--[[ Bind the key/val with the given node, and insert the node into the Hashtab. + NOTE: this function does not touch any queue +]] +local function insert_key(self, key, val, node) + -- Bind the key/val with the node + local node_id = node.id + self.key_v[node_id] = key + self.val_v[node_id] = val + + -- Insert the node into the hash-table + local key_hash = hash_string(self, key) + local bucket_v = self.bucket_v + node.conflict = bucket_v[key_hash] + bucket_v[key_hash] = node_id + self.num_items = self.num_items + 1 +end + + +function _M.get(self, key) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = find_key(self, key) + if not node_id then + return nil + end + + -- print(key, ": moving node ", tostring(node), " to cache queue head") + local cache_queue = self.cache_queue + local node = self.node_v + node_id + queue_remove(node) + queue_insert_head(cache_queue, node) + + local expire = node.expire + if expire >= 0 and expire < ngx_now() then + -- print("expired: ", node.expire, " > ", ngx_now()) + return nil, self.val_v[node_id], node.user_flags + end + + return self.val_v[node_id], nil, node.user_flags +end + + +function _M.delete(self, key) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = remove_key(self, key); + if not node_id then + return false + end + + local node = self.node_v + node_id + queue_remove(node) + queue_insert_tail(self.free_queue, node) + return true +end + + +function _M.set(self, key, value, ttl, flags) + if type(key) ~= "string" then + key = tostring(key) + end + + local node_id = find_key(self, key) + local node + if not node_id then + local free_queue = self.free_queue + if queue_is_empty(free_queue) then + -- evict the least recently used key + -- assert(not queue_is_empty(self.cache_queue)) + node = queue_last(self.cache_queue) + remove_key(self, self.key_v[node.id]) + else + -- take a free queue node + node = queue_head(free_queue) + -- print(key, ": get a new free node: ", tostring(node)) + end + + -- insert the key + insert_key(self, key, value, node) + else + node = self.node_v + node_id + self.val_v[node_id] = value + end + + queue_remove(node) + queue_insert_head(self.cache_queue, node) + + if ttl then + node.expire = ngx_now() + ttl + else + node.expire = -1 + end + + if type(flags) == "number" and flags >= 0 then + node.user_flags = flags + + else + node.user_flags = 0 + end +end + + +function _M.get_keys(self, max_count, res) + if not max_count or max_count == 0 then + max_count = self.num_items + end + + if not res then + res = new_tab(max_count + 1, 0) -- + 1 for trailing hole + end + + local cache_queue = self.cache_queue + local key_v = self.key_v + + local i = 0 + local node = queue_head(cache_queue) + + while node ~= cache_queue do + if i >= max_count then + break + end + + i = i + 1 + res[i] = key_v[node.id] + node = node.next + end + + res[i + 1] = nil + + return res +end + + +function _M.flush_all(self) + local cache_queue = self.cache_queue + local key_v = self.key_v + + local node = queue_head(cache_queue) + + while node ~= cache_queue do + local key = key_v[node.id] + node = node.next + _M.delete(self, key) + end +end + + +return _M