bunkerweb/lib/resty/session/shm.lua
Théophile Diot a3cd342f3e Squashed 'src/deps/src/lua-resty-session/' content from commit 8b5f8752f
git-subtree-dir: src/deps/src/lua-resty-session
git-subtree-split: 8b5f8752f3046396c414c5b97850e784c07e1641
2023-06-30 15:38:54 -04:00

315 lines
7.2 KiB
Lua

---
-- Shared Memory (SHM) backend for session library
--
-- @module resty.session.shm
local table_new = require "table.new"
local utils = require "resty.session.utils"
local meta_get_value = utils.meta_get_value
local meta_get_next = utils.meta_get_next
local get_name = utils.get_name
local errmsg = utils.errmsg
local setmetatable = setmetatable
local shared = ngx.shared
local random = math.random
local assert = assert
local error = error
local pairs = pairs
local max = math.max
local log = ngx.log
local WARN = ngx.WARN
local DEFAULT_ZONE = "sessions"
local CLEANUP_PROBABILITY = 0.1 -- 1 / 10
local function get_and_clean_metadata(dict, meta_key, current_time)
local size = dict:llen(meta_key)
if not size or size == 0 then
return
end
local max_expiry = current_time
local sessions = table_new(0, size)
for _ = 1, size do
local meta_value, err = dict:lpop(meta_key)
if not meta_value then
log(WARN, "[session] ", errmsg(err, "failed read meta value"))
break
end
local key, err, exp = meta_get_next(meta_value, 1)
if err then
return nil, err
end
if exp and exp > current_time then
sessions[key] = exp
max_expiry = max(max_expiry, exp)
else
sessions[key] = nil
end
end
for key, exp in pairs(sessions) do
local meta_value = meta_get_value(key, exp)
local ok, err = dict:rpush(meta_key, meta_value)
if not ok then
log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
end
end
local exp = max_expiry - current_time
if exp > 0 then
local ok, err = dict:expire(meta_key, max_expiry - current_time)
if not ok and err ~= "not found" then
log(WARN, "[session] ", errmsg(err, "failed to touch metadata"))
end
else
dict:delete(meta_key)
end
return sessions
end
local function cleanup(dict, meta_key, current_time)
get_and_clean_metadata(dict, meta_key, current_time)
end
local function read_metadata(self, meta_key, current_time)
return get_and_clean_metadata(self.dict, meta_key, current_time)
end
---
-- Storage
-- @section instance
local metatable = {}
metatable.__index = metatable
function metatable.__newindex()
error("attempt to update a read-only table", 2)
end
---
-- Store session data.
--
-- @function instance:set
-- @tparam string name cookie name
-- @tparam string key session key
-- @tparam string value session value
-- @tparam number ttl session ttl
-- @tparam number current_time current time
-- @tparam[opt] string old_key old session id
-- @tparam string stale_ttl stale ttl
-- @tparam[opt] table metadata table of metadata
-- @tparam boolean remember whether storing persistent session or not
-- @treturn true|nil ok
-- @treturn string error message
function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
local dict = self.dict
if not metadata and not old_key then
local ok, err = dict:set(get_name(self, name, key), value, ttl)
if not ok then
return nil, err
end
return true
end
local old_name, old_ttl
if old_key then
old_name = get_name(self, name, old_key)
if not remember then
old_ttl = dict:ttl(old_name)
end
end
local ok, err = dict:set(get_name(self, name, key), value, ttl)
if not ok then
return nil, err
end
if old_name then
if remember then
dict:delete(old_name)
elseif (not old_ttl or old_ttl > stale_ttl) then
local ok, err = dict:expire(old_name, stale_ttl)
if not ok then
log(WARN, "[session] ", errmsg(err, "failed to touch old session"))
end
end
end
if not metadata then
return true
end
local audiences = metadata.audiences
local subjects = metadata.subjects
local count = #audiences
for i = 1, count do
local meta_key = get_name(self, name, audiences[i], subjects[i])
local meta_value = meta_get_value(key, current_time + ttl)
local ok, err = dict:rpush(meta_key, meta_value)
if not ok then
log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
end
if old_key then
meta_value = meta_get_value(old_key, 0)
local ok, err = dict:rpush(meta_key, meta_value)
if not ok then
log(WARN, "[session] ", errmsg(err, "failed to update old metadata"))
end
end
-- no need to clean up every time we write
-- it is just beneficial when a key is used a lot
if random() < CLEANUP_PROBABILITY then
cleanup(dict, meta_key, current_time)
end
end
return true
end
---
-- Retrieve session data.
--
-- @function instance:get
-- @tparam string name cookie name
-- @tparam string key session key
-- @treturn string|nil session data
-- @treturn string error message
function metatable:get(name, key)
local value, err = self.dict:get(get_name(self, name, key))
if not value then
return nil, err
end
return value
end
---
-- Delete session data.
--
-- @function instance:delete
-- @tparam string name cookie name
-- @tparam string key session key
-- @tparam[opt] table metadata session meta data
-- @treturn boolean|nil session data
-- @treturn string error message
function metatable:delete(name, key, current_time, metadata)
local dict = self.dict
dict:delete(get_name(self, name, key))
if not metadata then
return true
end
local audiences = metadata.audiences
local subjects = metadata.subjects
local count = #audiences
for i = 1, count do
local meta_key = get_name(self, name, audiences[i], subjects[i])
local meta_value = meta_get_value(key, 0)
local ok, err = dict:rpush(meta_key, meta_value)
if not ok then
if not ok then
log(WARN, "[session] ", errmsg(err, "failed to update metadata"))
end
end
cleanup(dict, meta_key, current_time)
end
return true
end
---
-- Read session metadata.
--
-- @function instance:read_metadata
-- @tparam string name cookie name
-- @tparam string audience session key
-- @tparam string subject session key
-- @tparam number current_time current time
-- @treturn table|nil session metadata
-- @treturn string error message
function metatable:read_metadata(name, audience, subject, current_time)
local meta_key = get_name(self, name, audience, subject)
return read_metadata(self, meta_key, current_time)
end
local storage = {}
---
-- Configuration
-- @section configuration
---
-- Shared memory storage backend configuration
-- @field prefix Prefix for the keys stored in SHM.
-- @field suffix Suffix for the keys stored in SHM.
-- @field zone A name of shared memory zone (defaults to `sessions`).
-- @table configuration
---
-- Constructors
-- @section constructors
---
-- Create a SHM storage.
--
-- This creates a new shared memory storage instance.
--
-- @function module.new
-- @tparam[opt] table configuration shm storage @{configuration}
-- @treturn table shm storage instance
function storage.new(configuration)
local prefix = configuration and configuration.prefix
local suffix = configuration and configuration.suffix
local zone = configuration and configuration.zone or DEFAULT_ZONE
local dict = assert(shared[zone], "lua_shared_dict " .. zone .. " is missing")
return setmetatable({
prefix = prefix,
suffix = suffix,
dict = dict,
}, metatable)
end
return storage