mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
5f2aed616 chore(*): release 4.0.5 f4980f0e1 fix(dshm/memcached): add a missing return parameter as otherwise pool parameters are not respected git-subtree-dir: src/deps/src/lua-resty-session git-subtree-split: 5f2aed616d16fa7ca04dc40e23d6941740cd634d
2839 lines
80 KiB
Lua
2839 lines
80 KiB
Lua
---
|
|
-- Session library.
|
|
--
|
|
-- Session library provides HTTP session management capabilities for OpenResty based
|
|
-- applications, libraries and proxies.
|
|
--
|
|
-- @module resty.session
|
|
|
|
|
|
local require = require
|
|
|
|
|
|
local table_new = require "table.new"
|
|
local isempty = require "table.isempty"
|
|
local buffer = require "string.buffer"
|
|
local utils = require "resty.session.utils"
|
|
|
|
|
|
local clear_request_header = ngx.req.clear_header
|
|
local set_request_header = ngx.req.set_header
|
|
local setmetatable = setmetatable
|
|
local http_time = ngx.http_time
|
|
local tonumber = tonumber
|
|
local select = select
|
|
local assert = assert
|
|
local remove = table.remove
|
|
local header = ngx.header
|
|
local error = error
|
|
local floor = math.ceil
|
|
local time = ngx.time
|
|
local byte = string.byte
|
|
local type = type
|
|
local sub = string.sub
|
|
local fmt = string.format
|
|
local var = ngx.var
|
|
local log = ngx.log
|
|
local max = math.max
|
|
local min = math.min
|
|
|
|
|
|
local derive_aes_gcm_256_key_and_iv = utils.derive_aes_gcm_256_key_and_iv
|
|
local derive_hmac_sha256_key = utils.derive_hmac_sha256_key
|
|
local encrypt_aes_256_gcm = utils.encrypt_aes_256_gcm
|
|
local decrypt_aes_256_gcm = utils.decrypt_aes_256_gcm
|
|
local encode_base64url = utils.encode_base64url
|
|
local decode_base64url = utils.decode_base64url
|
|
local load_storage = utils.load_storage
|
|
local encode_json = utils.encode_json
|
|
local decode_json = utils.decode_json
|
|
local base64_size = utils.base64_size
|
|
local hmac_sha256 = utils.hmac_sha256
|
|
local rand_bytes = utils.rand_bytes
|
|
local unset_flag = utils.unset_flag
|
|
local set_flag = utils.set_flag
|
|
local has_flag = utils.has_flag
|
|
local inflate = utils.inflate
|
|
local deflate = utils.deflate
|
|
local bunpack = utils.bunpack
|
|
local errmsg = utils.errmsg
|
|
local sha256 = utils.sha256
|
|
local bpack = utils.bpack
|
|
local trim = utils.trim
|
|
|
|
|
|
local NOTICE = ngx.NOTICE
|
|
local WARN = ngx.WARN
|
|
|
|
|
|
local KEY_SIZE = 32
|
|
|
|
|
|
--[ HEADER ----------------------------------------------------------------------------------------------------] || [ PAYLOAD --]
|
|
--[ Type || Flags || Session ID || Creation Time || Rolling Offset || Data Size || Tag || Idling Offset || Mac ] || [ Data ]
|
|
--[ 1B || 2B || 32B || 5B || 4B || 3B || 16B || 3B || 16B ] || [ *B ]
|
|
|
|
|
|
local COOKIE_TYPE_SIZE = 1 -- 1
|
|
local FLAGS_SIZE = 2 -- 3
|
|
local SID_SIZE = 32 -- 35
|
|
local CREATION_TIME_SIZE = 5 -- 40
|
|
local ROLLING_OFFSET_SIZE = 4 -- 44
|
|
local DATA_SIZE = 3 -- 47
|
|
local TAG_SIZE = 16 -- 63
|
|
local IDLING_OFFSET_SIZE = 3 -- 66
|
|
local MAC_SIZE = 16 -- 82
|
|
|
|
|
|
local HEADER_TAG_SIZE = COOKIE_TYPE_SIZE + FLAGS_SIZE + SID_SIZE + CREATION_TIME_SIZE + ROLLING_OFFSET_SIZE + DATA_SIZE
|
|
local HEADER_TOUCH_SIZE = HEADER_TAG_SIZE + TAG_SIZE
|
|
local HEADER_MAC_SIZE = HEADER_TOUCH_SIZE + IDLING_OFFSET_SIZE
|
|
local HEADER_SIZE = HEADER_MAC_SIZE + MAC_SIZE
|
|
local HEADER_ENCODED_SIZE = base64_size(HEADER_SIZE)
|
|
|
|
|
|
local COOKIE_TYPE = bpack(COOKIE_TYPE_SIZE, 1)
|
|
|
|
|
|
local MAX_COOKIE_SIZE = 4096
|
|
local MAX_COOKIES = 9
|
|
local MAX_COOKIES_SIZE = MAX_COOKIES * MAX_COOKIE_SIZE -- 36864 bytes
|
|
local MAX_CREATION_TIME = 2 ^ (CREATION_TIME_SIZE * 8) - 1 -- ~34789 years
|
|
local MAX_ROLLING_OFFSET = 2 ^ (ROLLING_OFFSET_SIZE * 8) - 1 -- ~136 years
|
|
local MAX_IDLING_OFFSET = 2 ^ (IDLING_OFFSET_SIZE * 8) - 1 -- ~194 days
|
|
local MAX_DATA_SIZE = 2 ^ (DATA_SIZE * 8) - 1 -- 16777215 bytes
|
|
local MAX_TTL = 34560000 -- 400 days
|
|
-- see: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-11#section-4.1.2.1
|
|
|
|
|
|
local FLAGS_NONE = 0x0000
|
|
local FLAG_STORAGE = 0x0001
|
|
local FLAG_FORGET = 0x0002
|
|
local FLAG_DEFLATE = 0x0010
|
|
|
|
|
|
local DEFAULT_AUDIENCE = "default"
|
|
local DEFAULT_SUBJECT
|
|
local DEFAULT_ENFORCE_SAME_SUBJECT = false
|
|
local DEFAULT_META = {}
|
|
local DEFAULT_IKM
|
|
local DEFAULT_IKM_FALLBACKS
|
|
local DEFAULT_HASH_STORAGE_KEY = false
|
|
local DEFAULT_HASH_SUBJECT = false
|
|
local DEFAULT_STORE_METADATA = false
|
|
local DEFAULT_TOUCH_THRESHOLD = 60 -- 1 minute
|
|
local DEFAULT_COMPRESSION_THRESHOLD = 1024 -- 1 kB
|
|
local DEFAULT_REQUEST_HEADERS
|
|
local DEFAULT_RESPONSE_HEADERS
|
|
|
|
|
|
local DEFAULT_COOKIE_NAME = "session"
|
|
local DEFAULT_COOKIE_PATH = "/"
|
|
local DEFAULT_COOKIE_SAME_SITE = "Lax"
|
|
local DEFAULT_COOKIE_SAME_PARTY
|
|
local DEFAULT_COOKIE_PRIORITY
|
|
local DEFAULT_COOKIE_PARTITIONED
|
|
local DEFAULT_COOKIE_HTTP_ONLY = true
|
|
local DEFAULT_COOKIE_PREFIX
|
|
local DEFAULT_COOKIE_DOMAIN
|
|
local DEFAULT_COOKIE_SECURE
|
|
|
|
|
|
local DEFAULT_REMEMBER_COOKIE_NAME = "remember"
|
|
local DEFAULT_REMEMBER_SAFETY = "Medium"
|
|
local DEFAULT_REMEMBER_META = false
|
|
local DEFAULT_REMEMBER = false
|
|
|
|
|
|
local DEFAULT_STALE_TTL = 10 -- 10 seconds
|
|
local DEFAULT_IDLING_TIMEOUT = 900 -- 15 minutes
|
|
local DEFAULT_ROLLING_TIMEOUT = 3600 -- 1 hour
|
|
local DEFAULT_ABSOLUTE_TIMEOUT = 86400 -- 1 day
|
|
local DEFAULT_REMEMBER_ROLLING_TIMEOUT = 604800 -- 1 week
|
|
local DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT = 2592000 -- 30 days
|
|
|
|
|
|
local DEFAULT_STORAGE
|
|
|
|
|
|
local STATE_NEW = "new"
|
|
local STATE_OPEN = "open"
|
|
local STATE_CLOSED = "closed"
|
|
|
|
|
|
local AT_BYTE = byte("@")
|
|
local EQUALS_BYTE = byte("=")
|
|
local SEMICOLON_BYTE = byte(";")
|
|
|
|
|
|
local COOKIE_EXPIRE_FLAGS = "; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0"
|
|
|
|
|
|
local HEADER_BUFFER = buffer.new(HEADER_SIZE)
|
|
local FLAGS_BUFFER = buffer.new(128)
|
|
local DATA_BUFFER = buffer.new(MAX_COOKIES_SIZE)
|
|
local HIDE_BUFFER = buffer.new(256)
|
|
|
|
|
|
local DATA = table_new(2, 0)
|
|
|
|
|
|
local HEADERS = {
|
|
id = "Session-Id",
|
|
audience = "Session-Audience",
|
|
subject = "Session-Subject",
|
|
timeout = "Session-Timeout",
|
|
idling_timeout = "Session-Idling-Timeout",
|
|
["idling-timeout"] = "Session-Idling-Timeout",
|
|
rolling_timeout = "Session-Rolling-Timeout",
|
|
["rolling-timeout"] = "Session-Rolling-Timeout",
|
|
absolute_timeout = "Session-Absolute-Timeout",
|
|
["absolute-timeout"] = "Session-Absolute-Timeout",
|
|
}
|
|
|
|
|
|
local function set_response_header(name, value)
|
|
header[name] = value
|
|
end
|
|
|
|
|
|
local function sha256_storage_key(sid)
|
|
local key, err = sha256(sid)
|
|
if not key then
|
|
return nil, errmsg(err, "unable to sha256 hash session id")
|
|
end
|
|
|
|
if SID_SIZE ~= 32 then
|
|
key = sub(key, 1, SID_SIZE)
|
|
end
|
|
|
|
key, err = encode_base64url(key)
|
|
if not key then
|
|
return nil, errmsg(err, "unable to base64url encode session id")
|
|
end
|
|
|
|
return key
|
|
end
|
|
|
|
|
|
local function sha256_subject(subject)
|
|
local hashed_subject, err = sha256(subject)
|
|
if not hashed_subject then
|
|
return nil, errmsg(err, "unable to sha256 hash subject")
|
|
end
|
|
|
|
hashed_subject, err = encode_base64url(sub(hashed_subject, 1, 16))
|
|
if not hashed_subject then
|
|
return nil, errmsg(err, "unable to base64url encode subject")
|
|
end
|
|
|
|
return hashed_subject
|
|
end
|
|
|
|
|
|
local function calculate_mac(ikm, nonce, msg)
|
|
local mac_key, err = derive_hmac_sha256_key(ikm, nonce)
|
|
if not mac_key then
|
|
return nil, errmsg(err, "unable to derive session message authentication key")
|
|
end
|
|
|
|
local mac, err = hmac_sha256(mac_key, msg)
|
|
if not mac then
|
|
return nil, errmsg(err, "unable to calculate session message authentication code")
|
|
end
|
|
|
|
if MAC_SIZE ~= 32 then
|
|
return sub(mac, 1, MAC_SIZE)
|
|
end
|
|
|
|
return mac
|
|
end
|
|
|
|
|
|
local function calculate_cookie_chunks(cookie_name_size, data_size)
|
|
local space_needed = cookie_name_size + 1 + HEADER_ENCODED_SIZE + data_size
|
|
if space_needed > MAX_COOKIES_SIZE then
|
|
return nil, "cookie size limit exceeded"
|
|
end
|
|
|
|
if space_needed <= MAX_COOKIE_SIZE then
|
|
return 1
|
|
end
|
|
|
|
for i = 2, MAX_COOKIES do
|
|
space_needed = space_needed + cookie_name_size + 2
|
|
if space_needed > MAX_COOKIES_SIZE then
|
|
return nil, "cookie size limit exceeded"
|
|
elseif space_needed <= (MAX_COOKIE_SIZE * i) then
|
|
return i
|
|
end
|
|
end
|
|
|
|
return nil, "cookie size limit exceeded"
|
|
end
|
|
|
|
|
|
local function merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
|
|
if not cookies then
|
|
return cookie_data
|
|
end
|
|
|
|
if type(cookies) == "string" then
|
|
if byte(cookies, cookie_name_size + 1) == EQUALS_BYTE and
|
|
sub(cookies, 1, cookie_name_size) == cookie_name
|
|
then
|
|
return cookie_data
|
|
end
|
|
|
|
return { cookies, cookie_data }
|
|
end
|
|
|
|
if type(cookies) ~= "table" then
|
|
return nil, "unable to merge session cookies with response cookies"
|
|
end
|
|
|
|
local count = #cookies
|
|
for i = 1, count do
|
|
if byte(cookies[i], cookie_name_size + 1) == EQUALS_BYTE and
|
|
sub(cookies[i], 1, cookie_name_size) == cookie_name
|
|
then
|
|
cookies[i] = cookie_data
|
|
return cookies
|
|
end
|
|
|
|
if i == count then
|
|
cookies[i+1] = cookie_data
|
|
return cookies
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function get_store_metadata(self)
|
|
if not self.store_metadata then
|
|
return
|
|
end
|
|
|
|
local data = self.data
|
|
local count = #data
|
|
if count == 1 then
|
|
local audience = data[1][2]
|
|
local subject = data[1][3]
|
|
if audience and subject then
|
|
audience = encode_base64url(audience)
|
|
subject = self.hash_subject(subject)
|
|
return {
|
|
audiences = { audience },
|
|
subjects = { subject },
|
|
}
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
local audiences
|
|
local subjects
|
|
local index = 0
|
|
for i = 1, count do
|
|
local audience = data[i][2]
|
|
local subject = data[i][3]
|
|
if audience and subject then
|
|
audience = encode_base64url(audience)
|
|
self.hash_subject(subject)
|
|
if not audiences then
|
|
audiences = table_new(count, 0)
|
|
subjects = table_new(count, 0)
|
|
end
|
|
|
|
index = index + 1
|
|
audiences[index] = audience
|
|
subjects[index] = subject
|
|
end
|
|
end
|
|
|
|
if not audiences then
|
|
return
|
|
end
|
|
|
|
return {
|
|
audiences = audiences,
|
|
subjects = subjects,
|
|
}
|
|
end
|
|
|
|
|
|
local function get_property(self, name)
|
|
if name == "id" then
|
|
local sid = self.meta.sid
|
|
if not sid then
|
|
return
|
|
end
|
|
|
|
return encode_base64url(sid)
|
|
|
|
elseif name == "nonce" then
|
|
return self.meta.sid
|
|
|
|
elseif name == "audience" then
|
|
return self.data[self.data_index][2]
|
|
|
|
elseif name == "subject" then
|
|
return self.data[self.data_index][3]
|
|
|
|
elseif name == "timeout" then
|
|
local timeout
|
|
local meta = self.meta
|
|
|
|
if self.idling_timeout > 0 then
|
|
timeout = self.idling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset - meta.idling_offset)
|
|
end
|
|
|
|
if self.rolling_timeout > 0 then
|
|
local t = self.rolling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset)
|
|
timeout = timeout and min(t, timeout) or t
|
|
end
|
|
|
|
if self.absolute_timeout > 0 then
|
|
local t = self.absolute_timeout - (meta.timestamp - meta.creation_time)
|
|
timeout = timeout and min(t, timeout) or t
|
|
end
|
|
|
|
return timeout
|
|
|
|
elseif name == "idling-timeout" or name == "idling_timeout" then
|
|
local idling_timeout = self.idling_timeout
|
|
if idling_timeout == 0 then
|
|
return
|
|
end
|
|
|
|
local meta = self.meta
|
|
return idling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset - meta.idling_offset)
|
|
|
|
elseif name == "rolling-timeout" or name == "rolling_timeout" then
|
|
local rolling_timeout = self.rolling_timeout
|
|
if rolling_timeout == 0 then
|
|
return
|
|
end
|
|
|
|
local meta = self.meta
|
|
return rolling_timeout - (meta.timestamp - meta.creation_time - meta.rolling_offset)
|
|
|
|
elseif name == "absolute-timeout" or name == "absolute_timeout" then
|
|
local absolute_timeout = self.absolute_timeout
|
|
if absolute_timeout == 0 then
|
|
return
|
|
end
|
|
|
|
local meta = self.meta
|
|
return absolute_timeout - (meta.timestamp - meta.creation_time)
|
|
|
|
else
|
|
return self.meta[name]
|
|
end
|
|
end
|
|
|
|
|
|
local function open(self, remember, meta_only)
|
|
local storage = self.storage
|
|
local current_time = time()
|
|
local cookie_name
|
|
if remember then
|
|
cookie_name = self.remember_cookie_name
|
|
else
|
|
cookie_name = self.cookie_name
|
|
end
|
|
|
|
local cookie = var["cookie_" .. cookie_name]
|
|
if not cookie then
|
|
return nil, "missing session cookie"
|
|
end
|
|
|
|
local header_decoded do
|
|
header_decoded = sub(cookie, 1, HEADER_ENCODED_SIZE)
|
|
if #header_decoded ~= HEADER_ENCODED_SIZE then
|
|
return nil, "invalid session header"
|
|
end
|
|
local err
|
|
header_decoded, err = decode_base64url(header_decoded)
|
|
if not header_decoded then
|
|
return nil, errmsg(err, "unable to base64url decode session header")
|
|
end
|
|
end
|
|
|
|
HEADER_BUFFER:set(header_decoded)
|
|
|
|
local cookie_type do
|
|
cookie_type = HEADER_BUFFER:get(COOKIE_TYPE_SIZE)
|
|
if #cookie_type ~= COOKIE_TYPE_SIZE then
|
|
return nil, "invalid session cookie type"
|
|
end
|
|
if cookie_type ~= COOKIE_TYPE then
|
|
return nil, "invalid session cookie type"
|
|
end
|
|
end
|
|
|
|
local flags do
|
|
flags = HEADER_BUFFER:get(FLAGS_SIZE)
|
|
if #flags ~= FLAGS_SIZE then
|
|
return nil, "invalid session flags"
|
|
end
|
|
|
|
flags = bunpack(FLAGS_SIZE, flags)
|
|
|
|
if storage then
|
|
if not has_flag(flags, FLAG_STORAGE) then
|
|
return nil, "invalid session flags"
|
|
end
|
|
elseif has_flag(flags, FLAG_STORAGE) then
|
|
return nil, "invalid session flags"
|
|
end
|
|
end
|
|
|
|
local sid do
|
|
sid = HEADER_BUFFER:get(SID_SIZE)
|
|
if #sid ~= SID_SIZE then
|
|
return nil, "invalid session id"
|
|
end
|
|
end
|
|
|
|
local creation_time do
|
|
creation_time = HEADER_BUFFER:get(CREATION_TIME_SIZE)
|
|
if #creation_time ~= CREATION_TIME_SIZE then
|
|
return nil, "invalid session creation time"
|
|
end
|
|
|
|
creation_time = bunpack(CREATION_TIME_SIZE, creation_time)
|
|
if not creation_time or creation_time < 0 or creation_time > MAX_CREATION_TIME then
|
|
return nil, "invalid session creation time"
|
|
end
|
|
|
|
local absolute_elapsed = current_time - creation_time
|
|
if absolute_elapsed > MAX_ROLLING_OFFSET then
|
|
return nil, "session lifetime exceeded"
|
|
end
|
|
|
|
if remember then
|
|
local remember_absolute_timeout = self.remember_absolute_timeout
|
|
if remember_absolute_timeout ~= 0 then
|
|
if absolute_elapsed > remember_absolute_timeout then
|
|
return nil, "session remember absolute timeout exceeded"
|
|
end
|
|
end
|
|
|
|
else
|
|
local absolute_timeout = self.absolute_timeout
|
|
if absolute_timeout ~= 0 then
|
|
if absolute_elapsed > absolute_timeout then
|
|
return nil, "session absolute timeout exceeded"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local rolling_offset do
|
|
rolling_offset = HEADER_BUFFER:get(ROLLING_OFFSET_SIZE)
|
|
if #rolling_offset ~= ROLLING_OFFSET_SIZE then
|
|
return nil, "invalid session rolling offset"
|
|
end
|
|
|
|
rolling_offset = bunpack(ROLLING_OFFSET_SIZE, rolling_offset)
|
|
if not rolling_offset or rolling_offset < 0 or rolling_offset > MAX_ROLLING_OFFSET then
|
|
return nil, "invalid session rolling offset"
|
|
end
|
|
|
|
local rolling_elapsed = current_time - creation_time - rolling_offset
|
|
|
|
if remember then
|
|
local remember_rolling_timeout = self.remember_rolling_timeout
|
|
if remember_rolling_timeout ~= 0 then
|
|
if rolling_elapsed > remember_rolling_timeout then
|
|
return nil, "session remember rolling timeout exceeded"
|
|
end
|
|
end
|
|
|
|
else
|
|
local rolling_timeout = self.rolling_timeout
|
|
if rolling_timeout ~= 0 then
|
|
if rolling_elapsed > rolling_timeout then
|
|
return nil, "session rolling timeout exceeded"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local data_size do
|
|
data_size = HEADER_BUFFER:get(DATA_SIZE)
|
|
if #data_size ~= DATA_SIZE then
|
|
return nil, "invalid session data size"
|
|
end
|
|
|
|
data_size = bunpack(DATA_SIZE, data_size)
|
|
if not data_size or data_size < 0 or data_size > MAX_DATA_SIZE then
|
|
return nil, "invalid session data size"
|
|
end
|
|
end
|
|
|
|
local tag do
|
|
tag = HEADER_BUFFER:get(TAG_SIZE)
|
|
if #tag ~= TAG_SIZE then
|
|
return nil, "invalid session tag"
|
|
end
|
|
end
|
|
|
|
local idling_offset do
|
|
idling_offset = HEADER_BUFFER:get(IDLING_OFFSET_SIZE)
|
|
if #idling_offset ~= IDLING_OFFSET_SIZE then
|
|
return nil, "invalid session idling offset"
|
|
end
|
|
|
|
idling_offset = bunpack(IDLING_OFFSET_SIZE, idling_offset)
|
|
if not idling_offset or idling_offset < 0 or idling_offset > MAX_IDLING_OFFSET then
|
|
return nil, "invalid session idling offset"
|
|
end
|
|
|
|
if remember then
|
|
if idling_offset ~= 0 then
|
|
return nil, "invalid session idling offset"
|
|
end
|
|
|
|
else
|
|
local idling_timeout = self.idling_timeout
|
|
if idling_timeout ~= 0 then
|
|
local idling_elapsed = current_time - creation_time - rolling_offset - idling_offset
|
|
if idling_elapsed > idling_timeout then
|
|
return nil, "session idling timeout exceeded"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local ikm do
|
|
ikm = self.ikm
|
|
local mac = HEADER_BUFFER:get(MAC_SIZE)
|
|
if #mac ~= MAC_SIZE then
|
|
return nil, "invalid session message authentication code"
|
|
end
|
|
|
|
local msg = sub(header_decoded, 1, HEADER_MAC_SIZE)
|
|
local expected_mac, err = calculate_mac(ikm, sid, msg)
|
|
if mac ~= expected_mac then
|
|
local fallback_keys = self.ikm_fallbacks
|
|
if fallback_keys then
|
|
local count = #fallback_keys
|
|
if count > 0 then
|
|
for i = 1, count do
|
|
ikm = fallback_keys[i]
|
|
expected_mac, err = calculate_mac(ikm, sid, msg)
|
|
if mac == expected_mac then
|
|
break
|
|
end
|
|
|
|
if i == count then
|
|
return nil, errmsg(err, "invalid session message authentication code")
|
|
end
|
|
end
|
|
|
|
else
|
|
return nil, errmsg(err, "invalid session message authentication code")
|
|
end
|
|
|
|
else
|
|
return nil, errmsg(err, "invalid session message authentication code")
|
|
end
|
|
end
|
|
end
|
|
|
|
local data_index = self.data_index
|
|
local audience = self.data[data_index][2]
|
|
local initial_chunk, ciphertext, ciphertext_encoded, info_data do
|
|
if storage then
|
|
local key, err = self.hash_storage_key(sid)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
local data, err = storage:get(cookie_name, key, current_time)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to load session")
|
|
end
|
|
|
|
data, err = decode_json(data)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json decode session")
|
|
end
|
|
|
|
ciphertext = data[1]
|
|
ciphertext_encoded = ciphertext
|
|
info_data = data[2]
|
|
if info_data then
|
|
info_data, err = decode_base64url(info_data)
|
|
if not info_data then
|
|
return nil, errmsg(err, "unable to base64url decode session info")
|
|
end
|
|
|
|
info_data, err = decode_json(info_data)
|
|
if not info_data then
|
|
return nil, errmsg(err, "unable to json decode session info")
|
|
end
|
|
|
|
if not info_data[audience] then
|
|
info_data[audience] = self.info.data and self.info.data[audience] or nil
|
|
end
|
|
end
|
|
|
|
else
|
|
local cookie_chunks, err = calculate_cookie_chunks(#cookie_name, data_size)
|
|
if not cookie_chunks then
|
|
return nil, err
|
|
end
|
|
|
|
if cookie_chunks == 1 then
|
|
initial_chunk = sub(cookie, -data_size)
|
|
ciphertext = initial_chunk
|
|
|
|
else
|
|
initial_chunk = sub(cookie, HEADER_ENCODED_SIZE + 1)
|
|
DATA_BUFFER:reset():put(initial_chunk)
|
|
for i = 2, cookie_chunks do
|
|
local chunk = var["cookie_" .. cookie_name .. i]
|
|
if not chunk then
|
|
return nil, errmsg(err, "missing session cookie chunk")
|
|
end
|
|
|
|
DATA_BUFFER:put(chunk)
|
|
end
|
|
|
|
ciphertext = DATA_BUFFER:get()
|
|
end
|
|
end
|
|
|
|
if #ciphertext ~= data_size then
|
|
return nil, "invalid session payload"
|
|
end
|
|
|
|
local err
|
|
ciphertext, err = decode_base64url(ciphertext)
|
|
if not ciphertext then
|
|
return nil, errmsg(err, "unable to base64url decode session data")
|
|
end
|
|
end
|
|
|
|
if remember then
|
|
self.remember_meta = {
|
|
timestamp = current_time,
|
|
flags = flags,
|
|
sid = sid,
|
|
creation_time = creation_time,
|
|
rolling_offset = rolling_offset,
|
|
data_size = data_size,
|
|
idling_offset = idling_offset,
|
|
ikm = ikm,
|
|
header = header_decoded,
|
|
initial_chunk = initial_chunk,
|
|
ciphertext = ciphertext_encoded,
|
|
}
|
|
|
|
else
|
|
self.meta = {
|
|
timestamp = current_time,
|
|
flags = flags,
|
|
sid = sid,
|
|
creation_time = creation_time,
|
|
rolling_offset = rolling_offset,
|
|
data_size = data_size,
|
|
idling_offset = idling_offset,
|
|
ikm = ikm,
|
|
header = header_decoded,
|
|
initial_chunk = initial_chunk,
|
|
ciphertext = ciphertext_encoded,
|
|
}
|
|
end
|
|
|
|
if meta_only then
|
|
return true
|
|
end
|
|
|
|
local aes_key, err, iv
|
|
if remember then
|
|
aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid, self.remember_safety)
|
|
else
|
|
aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid)
|
|
end
|
|
|
|
if not aes_key then
|
|
return nil, errmsg(err, "unable to derive session decryption key")
|
|
end
|
|
|
|
local aad = sub(header_decoded, 1, HEADER_TAG_SIZE)
|
|
local plaintext, err = decrypt_aes_256_gcm(aes_key, iv, ciphertext, aad, tag)
|
|
if not plaintext then
|
|
return nil, errmsg(err, "unable to decrypt session data")
|
|
end
|
|
|
|
local data do
|
|
if has_flag(flags, FLAG_DEFLATE) then
|
|
plaintext, err = inflate(plaintext)
|
|
if not plaintext then
|
|
return nil, errmsg(err, "unable to inflate session data")
|
|
end
|
|
end
|
|
|
|
data, err = decode_json(plaintext)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json decode session data")
|
|
end
|
|
end
|
|
|
|
if storage then
|
|
self.info.data = info_data
|
|
end
|
|
|
|
local audience_index
|
|
local count = #data
|
|
for i = 1, count do
|
|
if data[i][2] == audience then
|
|
audience_index = i
|
|
break
|
|
end
|
|
end
|
|
|
|
if not audience_index then
|
|
data[count + 1] = self.data[data_index]
|
|
self.state = STATE_NEW
|
|
self.data = data
|
|
self.data_index = count + 1
|
|
return nil, "missing session audience", true
|
|
end
|
|
|
|
self.state = STATE_OPEN
|
|
self.data = data
|
|
self.data_index = audience_index
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function save(self, state, remember)
|
|
local cookie_name
|
|
local meta
|
|
if remember then
|
|
cookie_name = self.remember_cookie_name
|
|
meta = self.remember_meta or {}
|
|
else
|
|
cookie_name = self.cookie_name
|
|
meta = self.meta
|
|
end
|
|
|
|
local cookie_name_size = #cookie_name
|
|
local storage = self.storage
|
|
local flags = self.flags
|
|
|
|
if storage then
|
|
flags = set_flag(flags, FLAG_STORAGE)
|
|
else
|
|
flags = unset_flag(flags, FLAG_STORAGE)
|
|
end
|
|
|
|
local sid, err = rand_bytes(SID_SIZE)
|
|
if not sid then
|
|
return nil, errmsg(err, "unable to generate session id")
|
|
end
|
|
|
|
local current_time = time()
|
|
local rolling_offset
|
|
|
|
local creation_time = meta.creation_time
|
|
if creation_time then
|
|
rolling_offset = current_time - creation_time
|
|
if rolling_offset > MAX_ROLLING_OFFSET then
|
|
return nil, "session maximum rolling offset exceeded"
|
|
end
|
|
|
|
else
|
|
creation_time = current_time
|
|
rolling_offset = 0
|
|
end
|
|
|
|
if creation_time > MAX_CREATION_TIME then
|
|
-- this should only happen at around year 36759 (most likely a clock problem)
|
|
return nil, "session maximum creation time exceeded"
|
|
end
|
|
|
|
do
|
|
local meta_flags = meta.flags
|
|
if meta_flags and has_flag(meta_flags, FLAG_FORGET) then
|
|
flags = set_flag(flags, FLAG_FORGET)
|
|
end
|
|
end
|
|
|
|
local data, data_size, cookie_chunks do
|
|
data = self.data
|
|
if self.enforce_same_subject then
|
|
local count = #data
|
|
if count > 1 then
|
|
local subject = data[self.data_index][3]
|
|
for i = count, 1, -1 do
|
|
if data[i][3] ~= subject then
|
|
remove(data, i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
data, err = encode_json(data)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json encode session data")
|
|
end
|
|
|
|
data_size = #data
|
|
|
|
local compression_threshold = self.compression_threshold
|
|
if compression_threshold ~= 0 and data_size > compression_threshold then
|
|
local deflated_data, err = deflate(data)
|
|
if not deflated_data then
|
|
log(NOTICE, "[session] unable to deflate session data (", err , ")")
|
|
|
|
else
|
|
if deflated_data then
|
|
local deflated_size = #deflated_data
|
|
if deflated_size < data_size then
|
|
flags = set_flag(flags, FLAG_DEFLATE)
|
|
data = deflated_data
|
|
data_size = deflated_size
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
data_size = base64_size(data_size)
|
|
|
|
if storage then
|
|
if data_size > MAX_DATA_SIZE then
|
|
return nil, "session maximum data size exceeded"
|
|
end
|
|
|
|
cookie_chunks = 1
|
|
else
|
|
cookie_chunks, err = calculate_cookie_chunks(cookie_name_size, data_size)
|
|
if not cookie_chunks then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
|
|
local idling_offset = 0
|
|
|
|
local packed_flags = bpack(FLAGS_SIZE, flags)
|
|
local packed_data_size = bpack(DATA_SIZE, data_size)
|
|
local packed_creation_time = bpack(CREATION_TIME_SIZE, creation_time)
|
|
local packed_rolling_offset = bpack(ROLLING_OFFSET_SIZE, rolling_offset)
|
|
local packed_idling_offset = bpack(IDLING_OFFSET_SIZE, idling_offset)
|
|
|
|
HEADER_BUFFER:reset()
|
|
HEADER_BUFFER:put(COOKIE_TYPE, packed_flags, sid, packed_creation_time, packed_rolling_offset, packed_data_size)
|
|
|
|
local ikm = self.ikm
|
|
local aes_key, iv
|
|
if remember then
|
|
aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid, self.remember_safety)
|
|
else
|
|
aes_key, err, iv = derive_aes_gcm_256_key_and_iv(ikm, sid)
|
|
end
|
|
|
|
if not aes_key then
|
|
return nil, errmsg(err, "unable to derive session encryption key")
|
|
end
|
|
|
|
local ciphertext, err, tag = encrypt_aes_256_gcm(aes_key, iv, data, HEADER_BUFFER:tostring())
|
|
if not ciphertext then
|
|
return nil, errmsg(err, "unable to encrypt session data")
|
|
end
|
|
|
|
HEADER_BUFFER:put(tag, packed_idling_offset)
|
|
|
|
local mac, err = calculate_mac(ikm, sid, HEADER_BUFFER:tostring())
|
|
if not mac then
|
|
return nil, err
|
|
end
|
|
|
|
local header_decoded = HEADER_BUFFER:put(mac):get()
|
|
local header_encoded, err = encode_base64url(header_decoded)
|
|
if not header_encoded then
|
|
return nil, errmsg(err, "unable to base64url encode session header")
|
|
end
|
|
|
|
local payload, err = encode_base64url(ciphertext)
|
|
if not payload then
|
|
return nil, errmsg(err, "unable to base64url encode session data")
|
|
end
|
|
|
|
local cookies = header["Set-Cookie"]
|
|
local cookie_flags = self.cookie_flags
|
|
|
|
local initial_chunk
|
|
local ciphertext_encoded
|
|
|
|
local remember_flags
|
|
if remember then
|
|
local max_age = self.remember_rolling_timeout
|
|
if max_age == 0 or max_age > MAX_TTL then
|
|
max_age = MAX_TTL
|
|
end
|
|
|
|
local expires = http_time(creation_time + max_age)
|
|
remember_flags = fmt("; Expires=%s; Max-Age=%d", expires, max_age)
|
|
end
|
|
|
|
if cookie_chunks == 1 then
|
|
local cookie_data
|
|
if storage then
|
|
ciphertext_encoded = payload
|
|
if remember then
|
|
cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, cookie_flags, remember_flags)
|
|
else
|
|
cookie_data = fmt("%s=%s%s", cookie_name, header_encoded, cookie_flags)
|
|
end
|
|
|
|
else
|
|
initial_chunk = payload
|
|
if remember then
|
|
cookie_data = fmt("%s=%s%s%s%s", cookie_name, header_encoded, payload, cookie_flags, remember_flags)
|
|
else
|
|
cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, payload, cookie_flags)
|
|
end
|
|
end
|
|
|
|
cookies, err = merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
|
|
else
|
|
DATA_BUFFER:set(payload)
|
|
|
|
initial_chunk = DATA_BUFFER:get(MAX_COOKIE_SIZE - HEADER_ENCODED_SIZE - cookie_name_size - 1)
|
|
|
|
local cookie_data
|
|
if remember then
|
|
cookie_data = fmt("%s=%s%s%s%s", cookie_name, header_encoded, initial_chunk, cookie_flags, remember_flags)
|
|
else
|
|
cookie_data = fmt("%s=%s%s%s", cookie_name, header_encoded, initial_chunk, cookie_flags)
|
|
end
|
|
|
|
cookies, err = merge_cookies(cookies, cookie_name_size, cookie_name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
|
|
for i = 2, cookie_chunks do
|
|
local name = fmt("%s%d", cookie_name, i)
|
|
cookie_data = DATA_BUFFER:get(MAX_COOKIE_SIZE - cookie_name_size - 2)
|
|
if remember then
|
|
cookie_data = fmt("%s=%s%s%s", name, cookie_data, cookie_flags, remember_flags)
|
|
else
|
|
cookie_data = fmt("%s=%s%s", name, cookie_data, cookie_flags)
|
|
end
|
|
cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
|
|
if storage then
|
|
local key, err = self.hash_storage_key(sid)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
DATA[1] = payload
|
|
|
|
local info_data = self.info.data
|
|
if info_data then
|
|
info_data, err = encode_json(info_data)
|
|
if not info_data then
|
|
return nil, errmsg(err, "unable to json encode session info")
|
|
end
|
|
|
|
info_data, err = encode_base64url(info_data)
|
|
if not info_data then
|
|
return nil, errmsg(err, "unable to base64url encode session info")
|
|
end
|
|
|
|
DATA[2] = info_data
|
|
|
|
else
|
|
DATA[2] = nil
|
|
end
|
|
|
|
data, err = encode_json(DATA)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json encode session data")
|
|
end
|
|
|
|
local old_sid = meta.sid
|
|
local old_key
|
|
if old_sid then
|
|
old_key, err = self.hash_storage_key(old_sid)
|
|
if not old_key then
|
|
log(WARN, "[session] ", err)
|
|
end
|
|
end
|
|
|
|
local ttl = remember and self.remember_rolling_timeout or self.rolling_timeout
|
|
if ttl == 0 or ttl > MAX_TTL then
|
|
ttl = MAX_TTL
|
|
end
|
|
|
|
local store_metadata = get_store_metadata(self)
|
|
|
|
local ok, err = storage:set(cookie_name, key, data, ttl, current_time, old_key, self.stale_ttl, store_metadata, remember)
|
|
if not ok then
|
|
return nil, errmsg(err, "unable to store session data")
|
|
end
|
|
|
|
else
|
|
local old_data_size = meta.data_size
|
|
if old_data_size then
|
|
local old_cookie_chunks = calculate_cookie_chunks(cookie_name_size, old_data_size)
|
|
if old_cookie_chunks and old_cookie_chunks > cookie_chunks then
|
|
for i = cookie_chunks + 1, old_cookie_chunks do
|
|
local name = fmt("%s%d", cookie_name, i)
|
|
local cookie_data = fmt("%s=%s%s", name, cookie_flags, COOKIE_EXPIRE_FLAGS)
|
|
cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
header["Set-Cookie"] = cookies
|
|
|
|
if remember then
|
|
self.remember_meta = {
|
|
timestamp = current_time,
|
|
flags = flags,
|
|
sid = sid,
|
|
creation_time = creation_time,
|
|
rolling_offset = rolling_offset,
|
|
data_size = data_size,
|
|
idling_offset = idling_offset,
|
|
ikm = ikm,
|
|
header = header_decoded,
|
|
initial_chunk = initial_chunk,
|
|
ciphertext = ciphertext_encoded,
|
|
}
|
|
|
|
else
|
|
self.state = state or STATE_OPEN
|
|
self.meta = {
|
|
timestamp = current_time,
|
|
flags = flags,
|
|
sid = sid,
|
|
creation_time = creation_time,
|
|
rolling_offset = rolling_offset,
|
|
data_size = data_size,
|
|
idling_offset = idling_offset,
|
|
ikm = ikm,
|
|
header = header_decoded,
|
|
initial_chunk = initial_chunk,
|
|
ciphertext = ciphertext_encoded,
|
|
}
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function save_info(self, data, remember)
|
|
local cookie_name
|
|
local meta
|
|
if remember then
|
|
cookie_name = self.remember_cookie_name
|
|
meta = self.remember_meta or {}
|
|
else
|
|
cookie_name = self.cookie_name
|
|
meta = self.meta
|
|
end
|
|
|
|
local key, err = self.hash_storage_key(meta.sid)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
DATA[1] = meta.ciphertext
|
|
DATA[2] = data
|
|
|
|
data, err = encode_json(DATA)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json encode session data")
|
|
end
|
|
|
|
local current_time = time()
|
|
|
|
local ttl = self.rolling_timeout
|
|
if ttl == 0 or ttl > MAX_TTL then
|
|
ttl = MAX_TTL
|
|
end
|
|
ttl = max(ttl - (current_time - meta.creation_time - meta.rolling_offset), 1)
|
|
local ok, err = self.storage:set(cookie_name, key, data, ttl, current_time)
|
|
if not ok then
|
|
return nil, errmsg(err, "unable to store session info")
|
|
end
|
|
end
|
|
|
|
|
|
local function destroy(self, remember)
|
|
local cookie_name
|
|
local meta
|
|
if remember then
|
|
cookie_name = self.remember_cookie_name
|
|
meta = self.remember_meta or {}
|
|
else
|
|
cookie_name = self.cookie_name
|
|
meta = self.meta
|
|
end
|
|
|
|
local cookie_name_size = #cookie_name
|
|
local storage = self.storage
|
|
|
|
local cookie_chunks = 1
|
|
local data_size = meta.data_size
|
|
if not storage and data_size then
|
|
local err
|
|
cookie_chunks, err = calculate_cookie_chunks(cookie_name_size, data_size)
|
|
if not cookie_chunks then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
local cookie_flags = self.cookie_flags
|
|
local cookie_data = fmt("%s=%s%s", cookie_name, cookie_flags, COOKIE_EXPIRE_FLAGS)
|
|
local cookies, err = merge_cookies(header["Set-Cookie"], cookie_name_size, cookie_name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
|
|
if cookie_chunks > 1 then
|
|
for i = 2, cookie_chunks do
|
|
local name = fmt("%s%d", cookie_name, i)
|
|
cookie_data = fmt("%s=%s%s", name, cookie_flags, COOKIE_EXPIRE_FLAGS)
|
|
cookies, err = merge_cookies(cookies, cookie_name_size + 1, name, cookie_data)
|
|
if not cookies then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
|
|
if storage then
|
|
local sid = meta.sid
|
|
if sid then
|
|
local key, err = self.hash_storage_key(sid)
|
|
if not key then
|
|
return nil, err
|
|
end
|
|
|
|
local ok, err = storage:delete(cookie_name, key, meta.timestamp, get_store_metadata(self))
|
|
if not ok then
|
|
return nil, errmsg(err, "unable to destroy session")
|
|
end
|
|
end
|
|
end
|
|
|
|
header["Set-Cookie"] = cookies
|
|
|
|
self.state = STATE_CLOSED
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function clear_request_cookie(self, remember)
|
|
local cookies = var.http_cookie
|
|
if not cookies or cookies == "" then
|
|
return
|
|
end
|
|
|
|
local cookie_name
|
|
if remember then
|
|
cookie_name = self.remember_cookie_name
|
|
else
|
|
cookie_name = self.cookie_name
|
|
end
|
|
|
|
local cookie_name_size = #cookie_name
|
|
|
|
local cookie_chunks
|
|
if self.storage then
|
|
cookie_chunks = 1
|
|
else
|
|
local data_size = remember and
|
|
self.remember_meta and
|
|
self.remember_meta.data_size or
|
|
self.meta.data_size
|
|
cookie_chunks = calculate_cookie_chunks(cookie_name_size, data_size) or 1
|
|
end
|
|
|
|
HIDE_BUFFER:reset()
|
|
|
|
local size = #cookies
|
|
local name
|
|
local skip = false
|
|
local start = 1
|
|
for i = 1, size do
|
|
local b = byte(cookies, i)
|
|
if name then
|
|
if b == SEMICOLON_BYTE or i == size then
|
|
if not skip then
|
|
local value
|
|
if b == SEMICOLON_BYTE then
|
|
value = trim(sub(cookies, start, i - 1))
|
|
else
|
|
value = trim(sub(cookies, start))
|
|
end
|
|
|
|
if value ~= "" then
|
|
HIDE_BUFFER:put(value)
|
|
end
|
|
|
|
if i ~= size then
|
|
HIDE_BUFFER:put("; ")
|
|
end
|
|
end
|
|
|
|
if i == size then
|
|
break
|
|
end
|
|
|
|
name = nil
|
|
start = i + 1
|
|
skip = false
|
|
end
|
|
|
|
else
|
|
if b == EQUALS_BYTE or b == SEMICOLON_BYTE then
|
|
name = sub(cookies, start, i - 1)
|
|
elseif i == size then
|
|
name = sub(cookies, start, i)
|
|
end
|
|
|
|
if name then
|
|
name = trim(name)
|
|
if b == SEMICOLON_BYTE or i == size then
|
|
if name ~= "" then
|
|
HIDE_BUFFER:put(name)
|
|
if i ~= size then
|
|
HIDE_BUFFER:put(";")
|
|
end
|
|
|
|
elseif i == size then
|
|
break
|
|
end
|
|
|
|
name = nil
|
|
|
|
else
|
|
if name == cookie_name then
|
|
skip = true
|
|
|
|
elseif cookie_chunks > 1 then
|
|
local chunk_number = tonumber(sub(name, -1), 10)
|
|
if chunk_number and chunk_number > 1 and chunk_number <= cookie_chunks
|
|
and sub(name, 1, -2) == cookie_name
|
|
then
|
|
skip = true
|
|
end
|
|
end
|
|
|
|
if not skip then
|
|
if name ~= "" then
|
|
HIDE_BUFFER:put(name)
|
|
end
|
|
|
|
if b == EQUALS_BYTE then
|
|
HIDE_BUFFER:put("=")
|
|
end
|
|
end
|
|
end
|
|
|
|
start = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if #HIDE_BUFFER == 0 then
|
|
clear_request_header("Cookie")
|
|
else
|
|
set_request_header("Cookie", HIDE_BUFFER:get())
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
|
|
local function get_remember(self)
|
|
local flags = self.meta.flags
|
|
if flags and has_flag(flags, FLAG_FORGET) then
|
|
return false
|
|
end
|
|
|
|
if has_flag(self.flags, FLAG_FORGET) then
|
|
return false
|
|
end
|
|
|
|
return self.remember
|
|
end
|
|
|
|
|
|
local function set_property_header(self, property_header, set_header)
|
|
local name = HEADERS[property_header]
|
|
if not name then
|
|
return
|
|
end
|
|
|
|
local value = get_property(self, property_header)
|
|
if not value then
|
|
return
|
|
end
|
|
|
|
set_header(name, value)
|
|
end
|
|
|
|
|
|
local function set_property_headers(self, headers, set_header)
|
|
if not headers then
|
|
return
|
|
end
|
|
|
|
local count = #headers
|
|
if count == 0 then
|
|
return
|
|
end
|
|
|
|
for i = 1, count do
|
|
set_property_header(self, headers[i], set_header)
|
|
end
|
|
end
|
|
|
|
|
|
local function set_property_headers_vararg(self, set_header, count, ...)
|
|
if count == 1 then
|
|
local header_or_headers = select(1, ...)
|
|
if type(header_or_headers) == "table" then
|
|
return set_property_headers(self, header_or_headers, set_header)
|
|
end
|
|
|
|
return set_property_header(self, header_or_headers, set_header)
|
|
end
|
|
|
|
for i = 1, count do
|
|
local property_header = select(i, ...)
|
|
set_property_header(self, property_header, set_header)
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Session
|
|
-- @section instance
|
|
|
|
|
|
local fake_info_mt = {}
|
|
|
|
|
|
function fake_info_mt:set(key, value)
|
|
local session = self.session
|
|
assert(session.state ~= STATE_CLOSED, "unable to set session info on closed session")
|
|
session.data[session.data_index][1]["@" .. key] = value
|
|
end
|
|
|
|
|
|
function fake_info_mt:get(key)
|
|
local session = self.session
|
|
assert(session.state ~= STATE_CLOSED, "unable to get session info on closed session")
|
|
return session.data[session.data_index][1]["@" .. key]
|
|
end
|
|
|
|
|
|
function fake_info_mt:save()
|
|
local session = self.session
|
|
assert(session.state == STATE_OPEN, "unable to save session info on nonexistent or closed session")
|
|
return session:save()
|
|
end
|
|
|
|
|
|
fake_info_mt.__index = fake_info_mt
|
|
|
|
|
|
local fake_info = {}
|
|
|
|
|
|
function fake_info.new(session)
|
|
return setmetatable({
|
|
session = session,
|
|
data = false,
|
|
}, fake_info_mt)
|
|
end
|
|
|
|
|
|
local info_mt = {}
|
|
|
|
|
|
info_mt.__index = info_mt
|
|
|
|
|
|
---
|
|
-- Set a value in session information store.
|
|
--
|
|
-- @function instance.info:set
|
|
-- @tparam string key key
|
|
-- @param value value
|
|
function info_mt:set(key, value)
|
|
local session = self.session
|
|
assert(session.state ~= STATE_CLOSED, "unable to set session info on closed session")
|
|
local audience = session.data[session.data_index][2]
|
|
local data = self.data
|
|
if data then
|
|
if data[audience] then
|
|
data[audience][key] = value
|
|
|
|
else
|
|
data[audience] = {
|
|
[key] = value,
|
|
}
|
|
end
|
|
|
|
else
|
|
self.data = {
|
|
[audience] = {
|
|
[key] = value,
|
|
},
|
|
}
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Get a value from session information store.
|
|
--
|
|
-- @function instance.info:get
|
|
-- @tparam string key key
|
|
-- @return value
|
|
function info_mt:get(key)
|
|
local session = self.session
|
|
assert(session.state ~= STATE_CLOSED, "unable to get session info on closed session")
|
|
local data = self.data
|
|
if not data then
|
|
return
|
|
end
|
|
|
|
local audience = session.data[session.data_index][2]
|
|
data = self.data[audience]
|
|
if not data then
|
|
return
|
|
end
|
|
|
|
return data[key]
|
|
end
|
|
|
|
|
|
---
|
|
-- Save information.
|
|
--
|
|
-- Only updates backend storage. Does not send a new cookie.
|
|
--
|
|
-- @function instance.info:save
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function info_mt:save()
|
|
local session = self.session
|
|
assert(session.state == STATE_OPEN, "unable to save session info on nonexistent or closed session")
|
|
local data = self.data
|
|
if not data then
|
|
return true
|
|
end
|
|
|
|
local err
|
|
data, err = encode_json(data)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to json encode session info")
|
|
end
|
|
|
|
data, err = encode_base64url(data)
|
|
if not data then
|
|
return nil, errmsg(err, "unable to base64url encode session info")
|
|
end
|
|
|
|
local ok, err = save_info(session, data)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
if get_remember(session) then
|
|
if not session.remember_meta then
|
|
local remembered = open(self, true, true)
|
|
if not remembered then
|
|
return save(session, nil, true)
|
|
end
|
|
end
|
|
|
|
return save_info(session, data, true)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local info = {}
|
|
|
|
|
|
function info.new(session)
|
|
return setmetatable({
|
|
session = session,
|
|
data = false,
|
|
}, info_mt)
|
|
end
|
|
|
|
|
|
local metatable = {}
|
|
|
|
|
|
metatable.__index = metatable
|
|
|
|
|
|
function metatable.__newindex()
|
|
error("attempt to update a read-only table", 2)
|
|
end
|
|
|
|
|
|
---
|
|
-- Set session data.
|
|
--
|
|
-- @function instance:set_data
|
|
-- @tparam table data data
|
|
--
|
|
-- @usage
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if not exists then
|
|
-- session:set_data({
|
|
-- cart = {},
|
|
-- })
|
|
-- session:save()
|
|
-- end
|
|
function metatable:set_data(data)
|
|
assert(self.state ~= STATE_CLOSED, "unable to set session data on closed session")
|
|
assert(type(data) == "table", "invalid session data")
|
|
self.data[self.data_index][1] = data
|
|
end
|
|
|
|
|
|
---
|
|
-- Get session data.
|
|
--
|
|
-- @function instance:get_data
|
|
-- @treturn table value
|
|
--
|
|
-- @usage
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if exists then
|
|
-- local data = session:get_data()
|
|
-- ngx.req.set_header("Authorization", "Bearer " .. data.access_token)
|
|
-- end
|
|
function metatable:get_data()
|
|
assert(self.state ~= STATE_CLOSED, "unable to set session data on closed session")
|
|
return self.data[self.data_index][1]
|
|
end
|
|
|
|
|
|
---
|
|
-- Set a value in session.
|
|
--
|
|
-- @function instance:set
|
|
-- @tparam string key key
|
|
-- @param value value
|
|
--
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if not exists then
|
|
-- session:set("access-token", "eyJ...")
|
|
-- session:save()
|
|
-- end
|
|
function metatable:set(key, value)
|
|
assert(self.state ~= STATE_CLOSED, "unable to set session data value on closed session")
|
|
if self.storage or byte(key, 1) ~= AT_BYTE then
|
|
self.data[self.data_index][1][key] = value
|
|
else
|
|
self.data[self.data_index][1]["$" .. key] = value
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Get a value from session.
|
|
--
|
|
-- @function instance:get
|
|
-- @tparam string key key
|
|
-- @return value
|
|
--
|
|
-- @usage
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if exists then
|
|
-- local access_token = session:get("access-token")
|
|
-- ngx.req.set_header("Authorization", "Bearer " .. access_token)
|
|
-- end
|
|
function metatable:get(key)
|
|
assert(self.state ~= STATE_CLOSED, "unable to get session data value on closed session")
|
|
if self.storage or byte(key, 1) ~= AT_BYTE then
|
|
return self.data[self.data_index][1][key]
|
|
else
|
|
return self.data[self.data_index][1]["$" .. key]
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Set session audience.
|
|
--
|
|
-- @function instance:set_audience
|
|
-- @tparam string audience audience
|
|
--
|
|
-- @usage
|
|
-- local session = require "resty.session".new()
|
|
-- session.set_audience("my-service")
|
|
function metatable:set_audience(audience)
|
|
assert(self.state ~= STATE_CLOSED, "unable to set audience on closed session")
|
|
|
|
local data = self.data
|
|
local data_index = self.data_index
|
|
local current_audience = data[data_index][2]
|
|
if audience == current_audience then
|
|
return
|
|
end
|
|
|
|
local info_data = self.info.data
|
|
if info_data then
|
|
info_data[audience] = info_data[current_audience]
|
|
info_data[current_audience] = nil
|
|
end
|
|
|
|
local count = #data
|
|
if count == 1 then
|
|
data[1][2] = audience
|
|
return
|
|
end
|
|
|
|
local previous_index
|
|
for i = 1, count do
|
|
if data[i][2] == audience then
|
|
previous_index = i
|
|
break
|
|
end
|
|
end
|
|
|
|
data[data_index][2] = audience
|
|
|
|
if not previous_index or previous_index == data_index then
|
|
return
|
|
end
|
|
|
|
remove(data, previous_index)
|
|
|
|
if previous_index < data_index then
|
|
self.data_index = data_index - 1
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Get session audience.
|
|
--
|
|
-- @function instance:get_audience
|
|
-- @treturn string audience
|
|
function metatable:get_audience()
|
|
assert(self.state ~= STATE_CLOSED, "unable to get audience on closed session")
|
|
return self.data[self.data_index][2]
|
|
end
|
|
|
|
|
|
---
|
|
-- Set session subject.
|
|
--
|
|
-- @function instance:set_subject
|
|
-- @tparam string subject subject
|
|
--
|
|
-- @usage
|
|
-- local session = require "resty.session".new()
|
|
-- session.set_subject("john@doe.com")
|
|
function metatable:set_subject(subject)
|
|
assert(self.state ~= STATE_CLOSED, "unable to set subject on closed session")
|
|
self.data[self.data_index][3] = subject
|
|
end
|
|
|
|
|
|
---
|
|
-- Get session subject.
|
|
--
|
|
-- @function instance:get_subject
|
|
-- @treturn string subject
|
|
--
|
|
-- @usage
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if exists then
|
|
-- local subject = session.get_subject()
|
|
-- end
|
|
function metatable:get_subject()
|
|
assert(self.state ~= STATE_CLOSED, "unable to get subject on closed session")
|
|
return self.data[self.data_index][3]
|
|
end
|
|
|
|
|
|
---
|
|
-- Get session property.
|
|
--
|
|
-- Possible property names:
|
|
-- * `"id"`: 43 bytes session id (same as nonce, but base64 url-encoded)
|
|
-- * `"nonce"`: 32 bytes nonce (same as session id but in raw bytes)
|
|
-- * `"audience"`: Current session audience
|
|
-- * `"subject"`: Current session subject
|
|
-- * `"timeout"`: Closest timeout (in seconds) (what's left of it)
|
|
-- * `"idling-timeout`"`: Session idling timeout (in seconds) (what's left of it)
|
|
-- * `"rolling-timeout`"`: Session rolling timeout (in seconds) (what's left of it)
|
|
-- * `"absolute-timeout`"`: Session absolute timeout (in seconds) (what's left of it)
|
|
--
|
|
-- @function instance:get_property
|
|
-- @treturn string|number metadata
|
|
--
|
|
-- @usage
|
|
-- local session, err, exists = require "resty.session".open()
|
|
-- if exists then
|
|
-- local timeout = session.get_property("timeout")
|
|
-- end
|
|
function metatable:get_property(name)
|
|
assert(self.state ~= STATE_CLOSED, "unable to get session property on closed session")
|
|
return get_property(self, name)
|
|
end
|
|
|
|
|
|
---
|
|
-- Set persistent sessions on/off.
|
|
--
|
|
-- In many login forms user is given an option for "remember me".
|
|
-- You can call this function based on what user selected.
|
|
--
|
|
-- @function instance:set_remember
|
|
-- @tparam boolean value `true` to enable persistent session, `false` to disable them
|
|
function metatable:set_remember(value)
|
|
assert(self.state ~= STATE_CLOSED, "unable to set remember on closed session")
|
|
assert(type(value) == "boolean", "invalid remember value")
|
|
if value == false then
|
|
set_flag(self.flags, FLAG_FORGET)
|
|
else
|
|
unset_flag(self.flags, FLAG_FORGET)
|
|
end
|
|
|
|
self.remember = value
|
|
end
|
|
|
|
|
|
---
|
|
-- Get state of persistent sessions.
|
|
--
|
|
-- @function instance:get_remember
|
|
-- @treturn boolean `true` when persistent sessions are enabled, otherwise `false`
|
|
function metatable:get_remember()
|
|
assert(self.state ~= STATE_CLOSED, "unable to get remember on closed session")
|
|
return get_remember(self)
|
|
end
|
|
|
|
|
|
---
|
|
-- Open a session.
|
|
--
|
|
-- This can be used to open a session.
|
|
--
|
|
-- @function instance:open
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:open()
|
|
local exists, err, audience_error = open(self)
|
|
if exists then
|
|
return true
|
|
end
|
|
|
|
if audience_error then
|
|
return nil, err
|
|
end
|
|
|
|
if not self.remember then
|
|
return nil, err
|
|
end
|
|
|
|
local remembered = open(self, true)
|
|
if not remembered then
|
|
return nil, err
|
|
end
|
|
|
|
local ok, err = save(self, nil, true)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
self.state = STATE_NEW
|
|
self.meta = DEFAULT_META
|
|
|
|
local ok, err = save(self, STATE_OPEN)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Save the session.
|
|
--
|
|
-- Saves the session data and issues a new session cookie with a new session id.
|
|
-- When `remember` is enabled, it will also issue a new persistent cookie and
|
|
-- possibly save the data in backend store.
|
|
--
|
|
-- @function instance:save
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:save()
|
|
assert(self.state ~= STATE_CLOSED, "unable to save closed session")
|
|
|
|
local ok, err = save(self)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
if get_remember(self) then
|
|
if not self.remember_meta then
|
|
open(self, true, true)
|
|
end
|
|
|
|
local ok, err = save(self, nil, true)
|
|
if not ok then
|
|
log(WARN, "[session] ", err)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Touch the session.
|
|
--
|
|
-- Updates idling offset of the session by sending an updated session cookie.
|
|
-- It only sends the client cookie and never calls any backend session store
|
|
-- APIs. Normally the `session:refresh` is used to call this indirectly.
|
|
--
|
|
-- @function instance:touch
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:touch()
|
|
assert(self.state == STATE_OPEN, "unable to touch nonexistent or closed session")
|
|
|
|
local meta = self.meta
|
|
local idling_offset = min(time() - meta.creation_time - meta.rolling_offset, MAX_IDLING_OFFSET)
|
|
|
|
HEADER_BUFFER:reset():put(sub(meta.header, 1, HEADER_TOUCH_SIZE),
|
|
bpack(IDLING_OFFSET_SIZE, idling_offset))
|
|
|
|
local mac, err = calculate_mac(meta.ikm, meta.sid, HEADER_BUFFER:tostring())
|
|
if not mac then
|
|
return nil, err
|
|
end
|
|
|
|
local payload_header = HEADER_BUFFER:put(mac):get()
|
|
|
|
meta.idling_offset = idling_offset
|
|
meta.header = payload_header
|
|
|
|
payload_header, err = encode_base64url(payload_header)
|
|
if not payload_header then
|
|
return nil, errmsg(err, "unable to base64url encode session header")
|
|
end
|
|
|
|
local cookie_flags = self.cookie_flags
|
|
local cookie_name = self.cookie_name
|
|
local cookie_data
|
|
if self.storage then
|
|
cookie_data = fmt("%s=%s%s", cookie_name, payload_header, cookie_flags)
|
|
else
|
|
cookie_data = fmt("%s=%s%s%s", cookie_name, payload_header, meta.initial_chunk, cookie_flags)
|
|
end
|
|
|
|
header["Set-Cookie"] = merge_cookies(header["Set-Cookie"], #cookie_name, cookie_name, cookie_data)
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Refresh the session.
|
|
--
|
|
-- Either saves the session (creating a new session id) or touches the session
|
|
-- depending on whether the rolling timeout is getting closer, which means
|
|
-- by default when 3/4 of rolling timeout is spent - 45 minutes with default
|
|
-- rolling timeout of an hour. The touch has a threshold, by default one minute,
|
|
-- so it may be skipped in some cases (you can call `session:touch()` to force it).
|
|
--
|
|
-- @function instance:refresh
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:refresh()
|
|
assert(self.state == STATE_OPEN, "unable to refresh nonexistent or closed session")
|
|
|
|
local rolling_timeout = self.rolling_timeout
|
|
local idling_timeout = self.idling_timeout
|
|
if rolling_timeout == 0 and idling_timeout == 0 then
|
|
return true
|
|
end
|
|
|
|
local meta = self.meta
|
|
local rolling_elapsed = meta.timestamp - meta.creation_time - meta.rolling_offset
|
|
|
|
if rolling_timeout > 0 then
|
|
if rolling_elapsed > floor(rolling_timeout * 0.75) then
|
|
-- TODO: in case session was modified before calling this function, the possible remember me cookie needs to be saved too?
|
|
return save(self)
|
|
end
|
|
end
|
|
|
|
if idling_timeout > 0 then
|
|
local idling_elapsed = rolling_elapsed - meta.idling_offset
|
|
if idling_elapsed > self.touch_threshold then
|
|
return self:touch()
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Logout the session.
|
|
--
|
|
-- Logout either destroys the session or just clears the data for the current audience,
|
|
-- and saves it (logging out from the current audience).
|
|
--
|
|
-- @function instance:logout
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:logout()
|
|
assert(self.state == STATE_OPEN, "unable to logout nonexistent or closed session")
|
|
|
|
local data = self.data
|
|
if #data == 1 then
|
|
return self:destroy()
|
|
end
|
|
|
|
local data_index = self.data_index
|
|
local info_store = self.info
|
|
local info_data = info_store and info_store.data
|
|
if info_data then
|
|
local audience = data[data_index][2]
|
|
info_data[audience] = nil
|
|
if isempty(info_data) then
|
|
info_store.data = false
|
|
end
|
|
end
|
|
|
|
remove(data, data_index)
|
|
|
|
local ok, err = save(self, STATE_CLOSED)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
if get_remember(self) then
|
|
if not self.remember_meta then
|
|
open(self, true, true)
|
|
end
|
|
local ok, err = save(self, nil, true)
|
|
if not ok then
|
|
log(WARN, "[session] ", err)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Destroy the session.
|
|
--
|
|
-- Destroy the session and clear the cookies.
|
|
--
|
|
-- @function instance:destroy
|
|
-- @treturn true|nil ok
|
|
-- @treturn string error message
|
|
function metatable:destroy()
|
|
assert(self.state == STATE_OPEN, "unable to destroy nonexistent or closed session")
|
|
|
|
local ok, err = destroy(self)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
if get_remember(self) then
|
|
if not self.remember_meta then
|
|
local remembered = open(self, true, true)
|
|
if not remembered then
|
|
return true
|
|
end
|
|
end
|
|
|
|
ok, err = destroy(self, true)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
---
|
|
-- Close the session.
|
|
--
|
|
-- Just closes the session instance so that it cannot be used anymore.
|
|
--
|
|
-- @function instance:close
|
|
function metatable:close()
|
|
self.state = STATE_CLOSED
|
|
end
|
|
|
|
|
|
---
|
|
-- Clear the request session cookie.
|
|
--
|
|
-- Modifies the request headers by removing the session related
|
|
-- cookies. This is useful when you use the session library on
|
|
-- a proxy server and don't want the session cookies to be forwarded
|
|
-- to the upstream service.
|
|
--
|
|
-- @function instance:clear_request_cookie
|
|
function metatable:clear_request_cookie()
|
|
assert(self.state == STATE_OPEN, "unable to hide nonexistent or closed session")
|
|
|
|
local ok = clear_request_cookie(self)
|
|
if not ok then
|
|
log(NOTICE, "[session] unable to clear session request cookie")
|
|
end
|
|
|
|
if get_remember(self) then
|
|
ok = clear_request_cookie(self, true)
|
|
if not ok then
|
|
log(NOTICE, "[session] unable to clear persistent session request cookie")
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Sets request and response headers.
|
|
--
|
|
-- @function instance:set_headers
|
|
-- @tparam[opt] string ...
|
|
function metatable:set_headers(...)
|
|
assert(self.state == STATE_OPEN, "unable to set request/response headers of nonexistent or closed session")
|
|
|
|
local count = select("#", ...)
|
|
if count == 0 then
|
|
set_property_headers(self, self.request_headers, set_request_header)
|
|
set_property_headers(self, self.response_headers, set_response_header)
|
|
return
|
|
end
|
|
|
|
set_property_headers_vararg(self, set_request_header, count, ...)
|
|
set_property_headers_vararg(self, set_response_header, count, ...)
|
|
end
|
|
|
|
|
|
---
|
|
-- Set request headers.
|
|
--
|
|
-- @function instance:set_request_headers
|
|
-- @tparam[opt] string ...
|
|
function metatable:set_request_headers(...)
|
|
assert(self.state == STATE_OPEN, "unable to set request headers of nonexistent or closed session")
|
|
|
|
local count = select("#", ...)
|
|
if count == 0 then
|
|
set_property_headers(self, self.request_headers, set_request_header)
|
|
return
|
|
end
|
|
|
|
set_property_headers_vararg(self, set_request_header, count, ...)
|
|
end
|
|
|
|
|
|
---
|
|
-- Set response headers.
|
|
--
|
|
-- @function instance:set_response_headers
|
|
-- @tparam[opt] string ...
|
|
function metatable:set_response_headers(...)
|
|
assert(self.state == STATE_OPEN, "unable to set response headers of nonexistent or closed session")
|
|
|
|
local count = select("#", ...)
|
|
if count == 0 then
|
|
set_property_headers(self, self.response_headers, set_response_header)
|
|
return
|
|
end
|
|
|
|
set_property_headers_vararg(self, set_response_header, count, ...)
|
|
end
|
|
|
|
|
|
|
|
local session = {
|
|
_VERSION = "4.0.5",
|
|
metatable = metatable,
|
|
}
|
|
|
|
|
|
---
|
|
-- Configuration
|
|
-- @section configuration
|
|
|
|
|
|
---
|
|
-- Session configuration.
|
|
-- @field secret Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. `"RaJKp8UQW1"`.
|
|
-- @field secret_fallbacks Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. `{ "6RfrAYYzYq", "MkbTkkyF9C" }`.
|
|
-- @field ikm Initial key material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data. E.g. `"5ixIW4QVMk0dPtoIhn41Eh1I9enP2060"`
|
|
-- @field ikm_fallbacks Array of initial key materials that can be used as alternative keys (when doing key rotation), E.g. `{ "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }`.
|
|
-- @field cookie_prefix Cookie prefix, use `nil`, `"__Host-"` or `"__Secure-"` (defaults to `nil`)
|
|
-- @field cookie_name Session cookie name, e.g. `"session"` (defaults to `"session"`)
|
|
-- @field cookie_path Cookie path, e.g. `"/"` (defaults to `"/"`)
|
|
-- @field cookie_domain Cookie domain, e.g. `"example.com"` (defaults to `nil`)
|
|
-- @field cookie_http_only Mark cookie HTTP only, use `true` or `false` (defaults to `true`)
|
|
-- @field cookie_secure Mark cookie secure, use `nil`, `true` or `false` (defaults to `nil`)
|
|
-- @field cookie_priority Cookie priority, use `nil`, `"Low"`, `"Medium"`, or `"High"` (defaults to `nil`)
|
|
-- @field cookie_same_site Cookie same-site policy, use `nil`, `"Lax"`, `"Strict"`, `"None"`, or `"Default"` (defaults to `"Lax"`)
|
|
-- @field cookie_same_party Mark cookie with same party flag, use `nil`, `true`, or `false` (default: `nil`)
|
|
-- @field cookie_partitioned Mark cookie with partitioned flag, use `nil`, `true`, or `false` (default: `nil`)
|
|
-- @field remember Enable or disable persistent sessions, use `nil`, `true`, or `false` (defaults to `false`)
|
|
-- @field remember_safety Remember cookie key derivation complexity, use `nil`, `"None"` (fast), `"Low"`, `"Medium"`, `"High"` or `"Very High"` (slow) (defaults to `"Medium"`)
|
|
-- @field remember_cookie_name Persistent session cookie name, e.g. `"remember"` (defaults to `"remember"`)
|
|
-- @field audience Session audience, e.g. `"my-application"` (defaults to `"default"`)
|
|
-- @field subject Session subject, e.g. `"john.doe@example.com"` (defaults to `nil`)
|
|
-- @field enforce_same_subject When set to `true`, audiences need to share the same subject. The library removes non-subject matching audience data on save.
|
|
-- @field stale_ttl When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. `10` (defaults to `10`) (in seconds)
|
|
-- @field idling_timeout Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. `900` (defaults to `900`, or 15 minutes) (in seconds)
|
|
-- @field rolling_timeout Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. `3600` (defaults to `3600`, or an hour) (in seconds)
|
|
-- @field absolute_timeout Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. `86400` (defaults to `86400`, or a day) (in seconds)
|
|
-- @field remember_rolling_timeout Remember timeout specifies how long the persistent session is considered valid, e.g. `604800` (defaults to `604800`, or a week) (in seconds)
|
|
-- @field remember_absolute_timeout Remember absolute timeout limits how long the persistent session can be renewed, until re-authentication is required, e.g. `2592000` (defaults to `2592000`, or 30 days) (in seconds)
|
|
-- @field hash_storage_key Whether to hash or not the storage key. With storage key hashed it is impossible to decrypt data on server side without having a cookie too (defaults to `false`).
|
|
-- @field hash_subject Whether to hash or not the subject when `store_metadata` is enabled, e.g. for PII reasons (defaults to `false`).
|
|
-- @field store_metadata Whether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject (defaults to `false`).
|
|
-- @field touch_threshold Touch threshold controls how frequently or infrequently the `session:refresh` touches the cookie, e.g. `60` (defaults to `60`, or a minute) (in seconds)
|
|
-- @field compression_threshold Compression threshold controls when the data is deflated, e.g. `1024` (defaults to `1024`, or a kilobyte) (in bytes)
|
|
-- @field request_headers Set of headers to send to upstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` request headers when `set_headers` is called.
|
|
-- @field response_headers Set of headers to send to downstream, use `id`, `audience`, `subject`, `timeout`, `idling-timeout`, `rolling-timeout`, `absolute-timeout`. E.g. `{ "id", "timeout" }` will set `Session-Id` and `Session-Timeout` response headers when `set_headers` is called.
|
|
-- @field storage Storage is responsible of storing session data, use `nil` or `"cookie"` (data is stored in cookie), `"dshm"`, `"file"`, `"memcached"`, `"mysql"`, `"postgres"`, `"redis"`, or `"shm"`, or give a name of custom module (`"custom-storage"`), or a `table` that implements session storage interface (defaults to `nil`)
|
|
-- @field dshm Configuration for dshm storage, e.g. `{ prefix = "sessions" }`
|
|
-- @field file Configuration for file storage, e.g. `{ path = "/tmp", suffix = "session" }`
|
|
-- @field memcached Configuration for memcached storage, e.g. `{ prefix = "sessions" }`
|
|
-- @field mysql Configuration for MySQL / MariaDB storage, e.g. `{ database = "sessions" }`
|
|
-- @field postgres Configuration for Postgres storage, e.g. `{ database = "sessions" }`
|
|
-- @field redis Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. `{ prefix = "sessions" }`
|
|
-- @field shm Configuration for shared memory storage, e.g. `{ zone = "sessions" }`
|
|
-- @field ["custom-storage"] Custom storage (loaded with `require "custom-storage"`) configuration
|
|
-- @table configuration
|
|
|
|
|
|
---
|
|
-- Initialization
|
|
-- @section initialization
|
|
|
|
|
|
---
|
|
-- Initialize the session library.
|
|
--
|
|
-- This function can be called on `init` or `init_worker` phases on OpenResty
|
|
-- to set global default configuration to all session instances created by this
|
|
-- library.
|
|
--
|
|
-- @function module.init
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
--
|
|
-- @usage
|
|
-- require "resty.session".init({
|
|
-- audience = "my-application",
|
|
-- storage = "redis",
|
|
-- redis = {
|
|
-- username = "session",
|
|
-- password = "storage",
|
|
-- },
|
|
-- })
|
|
function session.init(configuration)
|
|
if configuration then
|
|
local ikm = configuration.ikm
|
|
if ikm then
|
|
assert(#ikm == KEY_SIZE, "invalid ikm size")
|
|
DEFAULT_IKM = ikm
|
|
|
|
else
|
|
local secret = configuration.secret
|
|
if secret then
|
|
DEFAULT_IKM = assert(sha256(secret))
|
|
end
|
|
end
|
|
|
|
local ikm_fallbacks = configuration.ikm_fallbacks
|
|
if ikm_fallbacks then
|
|
local count = #ikm_fallbacks
|
|
for i = 1, count do
|
|
assert(#ikm_fallbacks[i] == KEY_SIZE, "invalid ikm size in ikm_fallbacks")
|
|
end
|
|
|
|
DEFAULT_IKM_FALLBACKS = ikm_fallbacks
|
|
|
|
else
|
|
local secret_fallbacks = configuration.secret_fallbacks
|
|
if secret_fallbacks then
|
|
local count = #secret_fallbacks
|
|
if count > 0 then
|
|
DEFAULT_IKM_FALLBACKS = table_new(count, 0)
|
|
for i = 1, count do
|
|
DEFAULT_IKM_FALLBACKS[i] = assert(sha256(secret_fallbacks[i]))
|
|
end
|
|
|
|
else
|
|
DEFAULT_IKM_FALLBACKS = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local request_headers = configuration.request_headers
|
|
if request_headers then
|
|
local count = #request_headers
|
|
for i = 1, count do
|
|
assert(HEADERS[request_headers[i]], "invalid request header")
|
|
end
|
|
|
|
DEFAULT_REQUEST_HEADERS = request_headers
|
|
end
|
|
|
|
local response_headers = configuration.response_headers
|
|
if response_headers then
|
|
local count = #response_headers
|
|
for i = 1, count do
|
|
assert(HEADERS[response_headers[i]], "invalid response header")
|
|
end
|
|
|
|
DEFAULT_RESPONSE_HEADERS = response_headers
|
|
end
|
|
|
|
DEFAULT_COOKIE_NAME = configuration.cookie_name or DEFAULT_COOKIE_NAME
|
|
DEFAULT_COOKIE_PATH = configuration.cookie_path or DEFAULT_COOKIE_PATH
|
|
DEFAULT_COOKIE_DOMAIN = configuration.cookie_domain or DEFAULT_COOKIE_DOMAIN
|
|
DEFAULT_COOKIE_SAME_SITE = configuration.cookie_same_site or DEFAULT_COOKIE_SAME_SITE
|
|
DEFAULT_COOKIE_PRIORITY = configuration.cookie_priority or DEFAULT_COOKIE_PRIORITY
|
|
DEFAULT_COOKIE_PREFIX = configuration.cookie_prefix or DEFAULT_COOKIE_PREFIX
|
|
DEFAULT_REMEMBER_SAFETY = configuration.remember_safety or DEFAULT_REMEMBER_SAFETY
|
|
DEFAULT_REMEMBER_COOKIE_NAME = configuration.remember_cookie_name or DEFAULT_REMEMBER_COOKIE_NAME
|
|
DEFAULT_AUDIENCE = configuration.audience or DEFAULT_AUDIENCE
|
|
DEFAULT_SUBJECT = configuration.subject or DEFAULT_SUBJECT
|
|
DEFAULT_STALE_TTL = configuration.stale_ttl or DEFAULT_STALE_TTL
|
|
DEFAULT_IDLING_TIMEOUT = configuration.idling_timeout or DEFAULT_IDLING_TIMEOUT
|
|
DEFAULT_ROLLING_TIMEOUT = configuration.rolling_timeout or DEFAULT_ROLLING_TIMEOUT
|
|
DEFAULT_ABSOLUTE_TIMEOUT = configuration.absolute_timeout or DEFAULT_ABSOLUTE_TIMEOUT
|
|
DEFAULT_REMEMBER_ROLLING_TIMEOUT = configuration.remember_rolling_timeout or DEFAULT_REMEMBER_ROLLING_TIMEOUT
|
|
DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT = configuration.remember_absolute_timeout or DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT
|
|
DEFAULT_TOUCH_THRESHOLD = configuration.touch_threshold or DEFAULT_TOUCH_THRESHOLD
|
|
DEFAULT_COMPRESSION_THRESHOLD = configuration.compression_threshold or DEFAULT_COMPRESSION_THRESHOLD
|
|
DEFAULT_STORAGE = configuration.storage or DEFAULT_STORAGE
|
|
|
|
local cookie_http_only = configuration.cookie_http_only
|
|
if cookie_http_only ~= nil then
|
|
DEFAULT_COOKIE_HTTP_ONLY = cookie_http_only
|
|
end
|
|
|
|
local cookie_same_party = configuration.cookie_same_party
|
|
if cookie_same_party ~= nil then
|
|
DEFAULT_COOKIE_SAME_PARTY = cookie_same_party
|
|
end
|
|
|
|
local cookie_partitioned = configuration.cookie_partitioned
|
|
if cookie_partitioned ~= nil then
|
|
DEFAULT_COOKIE_PARTITIONED = cookie_partitioned
|
|
end
|
|
|
|
local cookie_secure = configuration.cookie_secure
|
|
if cookie_secure ~= nil then
|
|
DEFAULT_COOKIE_SECURE = cookie_secure
|
|
end
|
|
|
|
local remember = configuration.remember
|
|
if remember ~= nil then
|
|
DEFAULT_REMEMBER = remember
|
|
end
|
|
|
|
local hash_storage_key = configuration.hash_storage_key
|
|
if hash_storage_key ~= nil then
|
|
DEFAULT_HASH_STORAGE_KEY = hash_storage_key
|
|
end
|
|
|
|
local hash_subject = configuration.hash_subject
|
|
if hash_subject ~= nil then
|
|
DEFAULT_HASH_SUBJECT = hash_subject
|
|
end
|
|
|
|
local store_metadate = configuration.store_metadata
|
|
if store_metadate ~= nil then
|
|
DEFAULT_STORE_METADATA = store_metadate
|
|
end
|
|
|
|
local enforce_same_subject = configuration.enforce_same_subject
|
|
if enforce_same_subject ~= nil then
|
|
DEFAULT_ENFORCE_SAME_SUBJECT = enforce_same_subject
|
|
end
|
|
end
|
|
|
|
if not DEFAULT_IKM then
|
|
DEFAULT_IKM = assert(sha256(assert(rand_bytes(KEY_SIZE))))
|
|
end
|
|
|
|
if type(DEFAULT_STORAGE) == "string" then
|
|
DEFAULT_STORAGE = load_storage(DEFAULT_STORAGE, configuration)
|
|
end
|
|
end
|
|
|
|
|
|
---
|
|
-- Constructors
|
|
-- @section constructors
|
|
|
|
|
|
---
|
|
-- Create a new session.
|
|
--
|
|
-- This creates a new session instance.
|
|
--
|
|
-- @function module.new
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
-- @treturn table session instance
|
|
--
|
|
-- @usage
|
|
-- local session = require "resty.session".new()
|
|
-- -- OR
|
|
-- local session = require "resty.session".new({
|
|
-- audience = "my-application",
|
|
-- })
|
|
function session.new(configuration)
|
|
local cookie_name = configuration and configuration.cookie_name or DEFAULT_COOKIE_NAME
|
|
local cookie_path = configuration and configuration.cookie_path or DEFAULT_COOKIE_PATH
|
|
local cookie_domain = configuration and configuration.cookie_domain or DEFAULT_COOKIE_DOMAIN
|
|
local cookie_same_site = configuration and configuration.cookie_same_site or DEFAULT_COOKIE_SAME_SITE
|
|
local cookie_priority = configuration and configuration.cookie_priority or DEFAULT_COOKIE_PRIORITY
|
|
local cookie_prefix = configuration and configuration.cookie_prefix or DEFAULT_COOKIE_PREFIX
|
|
local remember_safety = configuration and configuration.remember_safety or DEFAULT_REMEMBER_SAFETY
|
|
local remember_cookie_name = configuration and configuration.remember_cookie_name or DEFAULT_REMEMBER_COOKIE_NAME
|
|
local audience = configuration and configuration.audience or DEFAULT_AUDIENCE
|
|
local subject = configuration and configuration.subject or DEFAULT_SUBJECT
|
|
local stale_ttl = configuration and configuration.stale_ttl or DEFAULT_STALE_TTL
|
|
local idling_timeout = configuration and configuration.idling_timeout or DEFAULT_IDLING_TIMEOUT
|
|
local rolling_timeout = configuration and configuration.rolling_timeout or DEFAULT_ROLLING_TIMEOUT
|
|
local absolute_timeout = configuration and configuration.absolute_timeout or DEFAULT_ABSOLUTE_TIMEOUT
|
|
local remember_rolling_timeout = configuration and configuration.remember_rolling_timeout or DEFAULT_REMEMBER_ROLLING_TIMEOUT
|
|
local remember_absolute_timeout = configuration and configuration.remember_absolute_timeout or DEFAULT_REMEMBER_ABSOLUTE_TIMEOUT
|
|
local touch_threshold = configuration and configuration.touch_threshold or DEFAULT_TOUCH_THRESHOLD
|
|
local compression_threshold = configuration and configuration.compression_threshold or DEFAULT_COMPRESSION_THRESHOLD
|
|
local storage = configuration and configuration.storage or DEFAULT_STORAGE
|
|
local ikm = configuration and configuration.ikm
|
|
local ikm_fallbacks = configuration and configuration.ikm_fallbacks
|
|
local request_headers = configuration and configuration.request_headers
|
|
local response_headers = configuration and configuration.response_headers
|
|
|
|
local cookie_http_only = configuration and configuration.cookie_http_only
|
|
if cookie_http_only == nil then
|
|
cookie_http_only = DEFAULT_COOKIE_HTTP_ONLY
|
|
end
|
|
|
|
local cookie_secure = configuration and configuration.cookie_secure
|
|
if cookie_secure == nil then
|
|
cookie_secure = DEFAULT_COOKIE_SECURE
|
|
end
|
|
|
|
local cookie_same_party = configuration and configuration.cookie_same_party
|
|
if cookie_same_party == nil then
|
|
cookie_same_party = DEFAULT_COOKIE_SAME_PARTY
|
|
end
|
|
|
|
local cookie_partitioned = configuration and configuration.cookie_partitioned
|
|
if cookie_partitioned == nil then
|
|
cookie_partitioned = DEFAULT_COOKIE_PARTITIONED
|
|
end
|
|
|
|
local remember = configuration and configuration.remember
|
|
if remember == nil then
|
|
remember = DEFAULT_REMEMBER
|
|
end
|
|
|
|
local hash_storage_key = configuration and configuration.hash_storage_key
|
|
if hash_storage_key == nil then
|
|
hash_storage_key = DEFAULT_HASH_STORAGE_KEY
|
|
end
|
|
|
|
local hash_subject = configuration and configuration.hash_subject
|
|
if hash_subject == nil then
|
|
hash_subject = DEFAULT_HASH_SUBJECT
|
|
end
|
|
|
|
local store_metadata = configuration and configuration.store_metadata
|
|
if store_metadata == nil then
|
|
store_metadata = DEFAULT_STORE_METADATA
|
|
end
|
|
|
|
local enforce_same_subject = configuration and configuration.enforce_same_subject
|
|
if enforce_same_subject == nil then
|
|
enforce_same_subject = DEFAULT_ENFORCE_SAME_SUBJECT
|
|
end
|
|
|
|
if cookie_prefix == "__Host-" then
|
|
cookie_name = cookie_prefix .. cookie_name
|
|
remember_cookie_name = cookie_prefix .. remember_cookie_name
|
|
cookie_path = DEFAULT_COOKIE_PATH
|
|
cookie_domain = nil
|
|
cookie_secure = true
|
|
|
|
elseif cookie_prefix == "__Secure-" then
|
|
cookie_name = cookie_prefix .. cookie_name
|
|
remember_cookie_name = cookie_prefix .. remember_cookie_name
|
|
cookie_secure = true
|
|
|
|
elseif cookie_same_site == "None" then
|
|
cookie_secure = true
|
|
end
|
|
|
|
if cookie_same_party then
|
|
assert(cookie_same_site ~= "Strict", "SameParty session cookies cannot use SameSite=Strict")
|
|
cookie_secure = true
|
|
end
|
|
|
|
FLAGS_BUFFER:reset()
|
|
|
|
if cookie_domain and cookie_domain ~= "localhost" and cookie_domain ~= "" then
|
|
FLAGS_BUFFER:put("; Domain=", cookie_domain)
|
|
end
|
|
|
|
FLAGS_BUFFER:put("; Path=", cookie_path, "; SameSite=", cookie_same_site)
|
|
|
|
if cookie_priority then
|
|
FLAGS_BUFFER:put("; Priority=", cookie_priority)
|
|
end
|
|
|
|
if cookie_same_party then
|
|
FLAGS_BUFFER:put("; SameParty")
|
|
end
|
|
|
|
if cookie_partitioned then
|
|
FLAGS_BUFFER:put("; Partitioned")
|
|
end
|
|
|
|
if cookie_secure then
|
|
FLAGS_BUFFER:put("; Secure")
|
|
end
|
|
|
|
if cookie_http_only then
|
|
FLAGS_BUFFER:put("; HttpOnly")
|
|
end
|
|
|
|
local cookie_flags = FLAGS_BUFFER:get()
|
|
|
|
if ikm then
|
|
assert(#ikm == KEY_SIZE, "invalid ikm size")
|
|
|
|
else
|
|
local secret = configuration and configuration.secret
|
|
if secret then
|
|
ikm = assert(sha256(secret))
|
|
|
|
else
|
|
if not DEFAULT_IKM then
|
|
DEFAULT_IKM = assert(sha256(assert(rand_bytes(KEY_SIZE))))
|
|
end
|
|
|
|
ikm = DEFAULT_IKM
|
|
end
|
|
end
|
|
|
|
if ikm_fallbacks then
|
|
local count = #ikm_fallbacks
|
|
for i = 1, count do
|
|
assert(#ikm_fallbacks[i] == KEY_SIZE, "invalid ikm size in ikm_fallbacks")
|
|
end
|
|
|
|
else
|
|
local secret_fallbacks = configuration and configuration.secret_fallbacks
|
|
if secret_fallbacks then
|
|
local count = #secret_fallbacks
|
|
if count > 0 then
|
|
ikm_fallbacks = table_new(count, 0)
|
|
for i = 1, count do
|
|
ikm_fallbacks[i] = assert(sha256(secret_fallbacks[i]))
|
|
end
|
|
end
|
|
|
|
else
|
|
ikm_fallbacks = DEFAULT_IKM_FALLBACKS
|
|
end
|
|
end
|
|
|
|
if request_headers then
|
|
local count = #request_headers
|
|
for i = 1, count do
|
|
assert(HEADERS[request_headers[i]], "invalid request header")
|
|
end
|
|
|
|
else
|
|
request_headers = DEFAULT_REQUEST_HEADERS
|
|
end
|
|
|
|
if response_headers then
|
|
local count = #response_headers
|
|
for i = 1, count do
|
|
assert(HEADERS[response_headers[i]], "invalid response header")
|
|
end
|
|
|
|
else
|
|
response_headers = DEFAULT_RESPONSE_HEADERS
|
|
end
|
|
|
|
local t = type(storage)
|
|
if t == "string" then
|
|
storage = load_storage(storage, configuration)
|
|
|
|
elseif t ~= "table" then
|
|
assert(storage == nil, "invalid session storage")
|
|
end
|
|
|
|
local self = setmetatable({
|
|
stale_ttl = stale_ttl,
|
|
idling_timeout = idling_timeout,
|
|
rolling_timeout = rolling_timeout,
|
|
absolute_timeout = absolute_timeout,
|
|
remember_rolling_timeout = remember_rolling_timeout,
|
|
remember_absolute_timeout = remember_absolute_timeout,
|
|
touch_threshold = touch_threshold,
|
|
compression_threshold = compression_threshold,
|
|
hash_storage_key = hash_storage_key and sha256_storage_key or encode_base64url,
|
|
hash_subject = hash_subject and sha256_subject or encode_base64url,
|
|
store_metadata = store_metadata,
|
|
enforce_same_subject = enforce_same_subject,
|
|
cookie_name = cookie_name,
|
|
cookie_flags = cookie_flags,
|
|
remember_cookie_name = remember_cookie_name,
|
|
remember_safety = remember_safety,
|
|
remember = remember,
|
|
flags = FLAGS_NONE,
|
|
storage = storage,
|
|
ikm = ikm,
|
|
ikm_fallbacks = ikm_fallbacks,
|
|
request_headers = request_headers,
|
|
response_headers = response_headers,
|
|
state = STATE_NEW,
|
|
meta = DEFAULT_META,
|
|
remember_meta = DEFAULT_REMEMBER_META,
|
|
info = info,
|
|
data_index = 1,
|
|
data = {
|
|
{
|
|
{},
|
|
audience,
|
|
subject,
|
|
},
|
|
},
|
|
}, metatable)
|
|
|
|
if storage then
|
|
self.info = info.new(self)
|
|
else
|
|
self.info = fake_info.new(self)
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
|
|
---
|
|
-- Helpers
|
|
-- @section helpers
|
|
|
|
|
|
---
|
|
-- Open a session.
|
|
--
|
|
-- This can be used to open a session, and it will either return an existing
|
|
-- session or a new session.
|
|
--
|
|
-- @function module.open
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
-- @treturn table session instance
|
|
-- @treturn string error message
|
|
-- @treturn boolean `true`, if session existed, otherwise `false`
|
|
--
|
|
-- @usage
|
|
-- local session = require "resty.session".open()
|
|
-- -- OR
|
|
-- local session, err, exists = require "resty.session".open({
|
|
-- audience = "my-application",
|
|
-- })
|
|
function session.open(configuration)
|
|
local self = session.new(configuration)
|
|
local exists, err = self:open()
|
|
if not exists then
|
|
return self, err, false
|
|
end
|
|
|
|
return self, err, true
|
|
end
|
|
|
|
|
|
---
|
|
-- Start a session and refresh it as needed.
|
|
--
|
|
-- This can be used to start a session, and it will either return an existing
|
|
-- session or a new session. In case there is an existing session, the
|
|
-- session will be refreshed as well (as needed).
|
|
--
|
|
-- @function module.start
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
-- @treturn table session instance
|
|
-- @treturn string error message
|
|
-- @treturn boolean `true`, if session existed, otherwise `false`
|
|
-- @treturn boolean `true`, if session was refreshed, otherwise `false`
|
|
--
|
|
-- @usage
|
|
-- local session = require "resty.session".start()
|
|
-- -- OR
|
|
-- local session, err, exists, refreshed = require "resty.session".start({
|
|
-- audience = "my-application",
|
|
-- })
|
|
function session.start(configuration)
|
|
local self, err, exists = session.open(configuration)
|
|
if not exists then
|
|
return self, err, false, false
|
|
end
|
|
|
|
local refreshed, err = self:refresh()
|
|
if not refreshed then
|
|
return self, err, true, false
|
|
end
|
|
|
|
return self, nil, true, true
|
|
end
|
|
|
|
|
|
---
|
|
-- Logout a session.
|
|
--
|
|
-- It logouts from a specific audience.
|
|
--
|
|
-- A single session cookie may be shared between multiple audiences
|
|
-- (or applications), thus there is a need to be able to logout from
|
|
-- just a single audience while keeping the session for the other
|
|
-- audiences.
|
|
--
|
|
-- When there is only a single audience, then this can be considered
|
|
-- equal to `session.destroy`.
|
|
--
|
|
-- When the last audience is logged out, the cookie will be destroyed
|
|
-- as well and invalidated on a client.
|
|
--
|
|
-- @function module.logout
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
-- @treturn boolean `true` session exists for an audience and was logged out successfully, otherwise `false`
|
|
-- @treturn string error message
|
|
-- @treturn boolean `true` if session existed, otherwise `false`
|
|
-- @treturn boolean `true` if session was logged out, otherwise `false`
|
|
--
|
|
-- @usage
|
|
-- require "resty.session".logout()
|
|
-- -- OR
|
|
-- local ok, err, exists, logged_out = require "resty.session".logout({
|
|
-- audience = "my-application",
|
|
-- })
|
|
function session.logout(configuration)
|
|
local self, err, exists = session.open(configuration)
|
|
if not exists then
|
|
return nil, err, false, false
|
|
end
|
|
|
|
local ok, err = self:logout()
|
|
if not ok then
|
|
return nil, err, true, false
|
|
end
|
|
|
|
return true, nil, true, true
|
|
end
|
|
|
|
---
|
|
-- Destroy a session.
|
|
--
|
|
-- It destroys the whole session and clears the cookies.
|
|
--
|
|
-- @function module.destroy
|
|
-- @tparam[opt] table configuration session @{configuration} overrides
|
|
-- @treturn boolean `true` session exists and was destroyed successfully, otherwise `nil`
|
|
-- @treturn string error message
|
|
-- @treturn boolean `true` if session existed, otherwise `false`
|
|
-- @treturn boolean `true` if session was destroyed, otherwise `false`
|
|
--
|
|
-- @usage
|
|
-- require "resty.session".destroy()
|
|
-- -- OR
|
|
-- local ok, err, exists, destroyed = require "resty.session".destroy({
|
|
-- cookie_name = "auth",
|
|
-- })
|
|
function session.destroy(configuration)
|
|
local self, err, exists = session.open(configuration)
|
|
if not exists then
|
|
return nil, err, false, false
|
|
end
|
|
|
|
local ok, err = self:destroy()
|
|
if not ok then
|
|
return nil, err, true, false
|
|
end
|
|
|
|
return true, nil, true, true
|
|
end
|
|
|
|
|
|
function session.__set_ngx_log(ngx_log)
|
|
log = ngx_log
|
|
end
|
|
|
|
|
|
function session.__set_ngx_var(ngx_var)
|
|
var = ngx_var
|
|
end
|
|
|
|
|
|
function session.__set_ngx_header(ngx_header)
|
|
header = ngx_header
|
|
end
|
|
|
|
|
|
function session.__set_ngx_req_clear_header(clear_header)
|
|
clear_request_header = clear_header
|
|
end
|
|
|
|
|
|
function session.__set_ngx_req_set_header(set_header)
|
|
set_request_header = set_header
|
|
end
|
|
|
|
|
|
return session
|