2023-06-30 19:38:54 +00:00
---
-- 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 = {
2023-09-15 12:22:59 +00:00
_VERSION = " 4.0.5 " ,
2023-06-30 19:38:54 +00:00
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