bunkerweb/lib/resty/session/mysql.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

379 lines
11 KiB
Lua

---
-- MySQL / MariaDB backend for session library
--
-- @module resty.session.mysql
---
-- Database
-- @section database
---
-- Sessions table.
--
-- Database table that stores session data.
--
-- @usage
-- CREATE TABLE IF NOT EXISTS sessions (
-- sid CHAR(43) PRIMARY KEY,
-- name VARCHAR(255),
-- data MEDIUMTEXT,
-- exp DATETIME,
-- INDEX (exp)
-- ) CHARACTER SET ascii;
-- @table sessions
---
-- Sessions metadata table.
--
-- This is only needed if you want to store session metadata.
--
-- @usage
-- CREATE TABLE IF NOT EXISTS sessions_meta (
-- aud VARCHAR(255),
-- sub VARCHAR(255),
-- sid CHAR(43),
-- PRIMARY KEY (aud, sub, sid),
-- CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE
-- ) CHARACTER SET ascii;
-- @table metadata
local buffer = require "string.buffer"
local mysql = require "resty.mysql"
local setmetatable = setmetatable
local random = math.random
local ipairs = ipairs
local error = error
local fmt = string.format
local DEFAULT_HOST = "127.0.0.1"
local DEFAULT_PORT = 3306
local DEFAULT_TABLE = "sessions"
local DEFAULT_CHARSET = "ascii"
local SET = "INSERT INTO %s (sid, name, data, exp) VALUES ('%s', '%s', '%s', FROM_UNIXTIME(%d)) AS new ON DUPLICATE KEY UPDATE data = new.data"
local SET_META_PREFIX = "INSERT INTO %s (aud, sub, sid) VALUES "
local SET_META_VALUES = "('%s', '%s', '%s')"
local SET_META_SUFFIX = " ON DUPLICATE KEY UPDATE sid = sid"
local GET_META = "SELECT sid, exp FROM %s JOIN %s USING (sid) WHERE aud = '%s' AND sub = '%s' AND exp >= FROM_UNIXTIME(%d)"
local GET = "SELECT data FROM %s WHERE sid = '%s' AND exp >= FROM_UNIXTIME(%d)"
local EXPIRE = "UPDATE %s SET exp = FROM_UNIXTIME(%d) WHERE sid = '%s' AND exp > FROM_UNIXTIME(%d)"
local DELETE = "DELETE FROM %s WHERE sid = '%s'"
local CLEANUP = "DELETE FROM %s WHERE exp < FROM_UNIXTIME(%d)"
local SQL = buffer.new()
local STM_DELIM = ";\n"
local VAL_DELIM = ", "
local CLEANUP_PROBABILITY = 0.001 -- 1 / 1000
local function exec(self, query)
local my = mysql:new()
local connect_timeout = self.connect_timeout
local send_timeout = self.send_timeout
local read_timeout = self.read_timeout
if connect_timeout or send_timeout or read_timeout then
if my.sock and my.sock.settimeouts then
my.sock:settimeouts(connect_timeout, send_timeout, read_timeout)
else
my:set_timeout(connect_timeout)
end
end
local ok, err = my:connect(self.options)
if not ok then
return nil, err
end
ok, err = my:query(query)
if not my:set_keepalive(self.keepalive_timeout) then
my:close()
end
return ok, err
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 table = self.table
local exp = ttl + current_time
if not metadata and not old_key then
return exec(self, fmt(SET, table, key, name, value, exp))
end
SQL:reset():putf(SET, table, key, name, value, exp)
if old_key then
if remember then
SQL:put(STM_DELIM):putf(DELETE, table, old_key)
else
local stale_exp = stale_ttl + current_time
SQL:put(STM_DELIM):putf(EXPIRE, table, stale_exp, old_key, stale_exp)
end
end
local table_meta = self.table_meta
if metadata then
local audiences = metadata.audiences
local subjects = metadata.subjects
local count = #audiences
SQL:put(STM_DELIM):putf(SET_META_PREFIX, table_meta)
for i = 1, count do
if i > 1 then
SQL:put(VAL_DELIM)
end
SQL:putf(SET_META_VALUES, audiences[i], subjects[i], key)
end
SQL:putf(SET_META_SUFFIX)
end
if random() < CLEANUP_PROBABILITY then
SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
end
return exec(self, SQL:get())
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, current_time) -- luacheck: ignore
local res, err = exec(self, fmt(GET, self.table, key, current_time))
if not res then
return nil, err
end
local row = res[1]
if not row then
return nil, "session not found"
end
local data = row.data
if not row.data then
return nil, "session not found"
end
return data
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) -- luacheck: ignore
SQL:reset():putf(DELETE, self.table, key)
if random() < CLEANUP_PROBABILITY then
SQL:put(STM_DELIM):putf(CLEANUP, self.table, current_time)
end
return exec(self, SQL:get())
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) -- luacheck: ignore
local res = {}
local t = exec(self, fmt(GET_META, self.table_meta, self.table, audience, subject, current_time))
if not t then
return nil, "not found"
end
for _, v in ipairs(t) do
local key = v.sid
if key then
res[key] = v.exp
end
end
return res
end
local storage = {}
---
-- Configuration
-- @section configuration
---
-- Postgres storage backend configuration
-- @field host The host to connect (defaults to `"127.0.0.1"`).
-- @field port The port to connect (defaults to `3306`).
-- @field socket The socket file to connect to (defaults to `nil`).
-- @field username The database username to authenticate (defaults to `nil`).
-- @field password Password for authentication, may be required depending on server configuration.
-- @field charset The character set used on the MySQL connection (defaults to `"ascii"`).
-- @field database The database name to connect.
-- @field table_name Name of database table to which to store session data (defaults to `"sessions"`).
-- @field table_name_meta Name of database meta data table to which to store session meta data (defaults to `"sessions_meta"`).
-- @field max_packet_size The upper limit for the reply packets sent from the MySQL server (defaults to 1 MB).
-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
-- @field send_timeout Controls the default timeout value used in TCP/unix-domain socket object's `send` method.
-- @field read_timeout Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.
-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
-- @field pool A custom name for the connection pool being used.
-- @field pool_size The size of the connection pool.
-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
-- @field ssl Enable SSL (defaults to `false`).
-- @field ssl_verify Verify server certificate (defaults to `nil`).
-- @table configuration
---
-- Constructors
-- @section constructors
---
-- Create a MySQL / MariaDB storage.
--
-- This creates a new MySQL / MariaDB storage instance.
--
-- @function module.new
-- @tparam[opt] table configuration mysql/mariadb storage @{configuration}
-- @treturn table mysql/mariadb storage instance
function storage.new(configuration)
local host = configuration and configuration.host or DEFAULT_HOST
local port = configuration and configuration.port or DEFAULT_PORT
local socket = configuration and configuration.socket
local username = configuration and configuration.username
local password = configuration and configuration.password
local charset = configuration and configuration.charset or DEFAULT_CHARSET
local database = configuration and configuration.database
local max_packet_size = configuration and configuration.max_packet_size
local table_name = configuration and configuration.table or DEFAULT_TABLE
local table_name_meta = configuration and configuration.table_meta
local connect_timeout = configuration and configuration.connect_timeout
local send_timeout = configuration and configuration.send_timeout
local read_timeout = configuration and configuration.read_timeout
local keepalive_timeout = configuration and configuration.keepalive_timeout
local pool = configuration and configuration.pool
local pool_size = configuration and configuration.pool_size
local backlog = configuration and configuration.backlog
local ssl = configuration and configuration.ssl
local ssl_verify = configuration and configuration.ssl_verify
if socket then
return setmetatable({
table = table_name,
table_meta = table_name_meta or (table_name .. "_meta"),
connect_timeout = connect_timeout,
send_timeout = send_timeout,
read_timeout = read_timeout,
keepalive_timeout = keepalive_timeout,
options = {
path = socket,
user = username,
password = password,
charset = charset,
database = database,
max_packet_size = max_packet_size,
pool = pool,
pool_size = pool_size,
backlog = backlog,
ssl = ssl,
ssl_verify = ssl_verify,
}
}, metatable)
end
return setmetatable({
table = table_name,
table_meta = table_name_meta or (table_name .. "_meta"),
connect_timeout = connect_timeout,
send_timeout = send_timeout,
read_timeout = read_timeout,
keepalive_timeout = keepalive_timeout,
options = {
host = host,
port = port,
user = username,
password = password,
charset = charset,
database = database,
max_packet_size = max_packet_size,
pool = pool,
pool_size = pool_size,
backlog = backlog,
ssl = ssl,
ssl_verify = ssl_verify,
}
}, metatable)
end
return storage